<template>
  <div class="ctms-page setup-page ctms-page-shell page--flush">
    <div class="setup-shell unified-shell">
      <div class="setup-header unified-action-bar">
      <div class="setup-flow">
        <div
          v-for="(step, index) in steps"
          :key="step.key"
          class="flow-step"
          :class="{ active: index === activeStep, done: isStepCompleted(index), 'has-error': stepHasErrors(index) }"
          @click="goStep(index)"
        >
          <span class="flow-index">{{ index + 1 }}</span>
          <span class="flow-label">{{ step.label }}</span>
          <el-icon v-if="isStepCompleted(index) && !stepHasErrors(index)" class="flow-done-icon"><SuccessFilled /></el-icon>
          <span v-if="index < steps.length - 1" class="flow-connector" />
        </div>
      </div>

      <div class="setup-toolbar">
        <div class="setup-toolbar-left">
          <div class="setup-view-mode">
            <el-radio-group v-model="setupViewMode" size="small">
              <el-radio-button label="draft">草稿</el-radio-button>
              <el-radio-button label="preview">预览</el-radio-button>
              <el-radio-button label="published" :disabled="!publishedSetupDraft">已发布版</el-radio-button>
            </el-radio-group>
          </div>
          
          <el-tag v-if="project?.is_locked" type="warning" effect="plain" class="locked-tag" size="small">
            <el-icon><Lock /></el-icon>
            已锁定
          </el-tag>

          <div class="setup-version-meta">
            <div class="meta-item">
              <span class="meta-label">当前发布</span>
              <span class="meta-group">
                <span class="meta-br">{{ setupPublishedBranchName || "main" }}</span>
                <span class="meta-ver">{{ setupPublishedVersionText }}</span>
              </span>
              <el-tooltip v-if="setupPublishedAt" :content="setupPublishedAt" placement="top">
                <span class="meta-time"><el-icon><Clock/></el-icon></span>
              </el-tooltip>
            </div>
            
            <div class="meta-item" v-if="!isPublishedView">
              <span class="meta-label">草稿基线</span>
              <span class="meta-group draft-group">
                <span class="meta-br">{{ setupDraftBranchDisplay }}</span>
                <span class="meta-ver">{{ setupDraftBaseVersionDisplay }}</span>
              </span>
              <el-tag v-if="setupDraftStatusDisplay !== '-'" size="small" :type="setupWorkflowTagType" class="meta-status">{{ setupDraftStatusDisplay }}</el-tag>
            </div>
          </div>
        </div>

        <div class="setup-toolbar-actions">
          <el-button v-if="canManageSetup" type="success" plain :loading="primarySaveLoading" :disabled="isPublishedView" @click="handlePrimarySaveConfig">保存配置</el-button>
          <el-button v-if="canManageSetup" type="info" plain :disabled="isPublishedView || !publishedSetupDraft" @click="handleRefillDraft">一键回填</el-button>
          <el-button v-if="canManageSetup" type="danger" plain :disabled="isPublishedView" @click="handleClearDraft">清空草稿</el-button>
          <el-button v-if="canManageSetup" type="warning" plain :loading="publishConfirmLoading" @click="publishConfigNow">发布版本</el-button>
          <el-button
            type="info"
            plain
            @click="openRollbackDialog"
          >
            版本管理
          </el-button>
          <el-dropdown trigger="click" class="setup-secondary-menu" @command="handleSecondaryAction">
            <el-button plain class="secondary-menu-btn">
              更多
              <el-icon class="el-icon--right"><ArrowDown /></el-icon>
            </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="enter">
                  <el-icon><Promotion /></el-icon>
                  进入项目
                </el-dropdown-item>
                <el-dropdown-item command="members">
                  <el-icon><User /></el-icon>
                  项目账号/成员配置
                </el-dropdown-item>
                <el-dropdown-item command="sites">
                  <el-icon><OfficeBuilding /></el-icon>
                  中心管理
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
      </div>
      </div>
      <div v-if="currentStepErrors.length" class="setup-step-errors">
        <el-alert
          type="error"
          show-icon
          :closable="false"
          :title="`当前步骤有 ${currentStepErrors.length} 项需修正`"
          :description="currentStepErrors[0]?.message"
        />
      </div>
      <div
        v-if="drawerVisible"
        class="setup-edit-backdrop"
        :class="{ 'is-closing': drawerClosing }"
        @click="closeEditDrawer"
      />
      <div
        class="setup-content unified-section"
        :class="{
          'setup-content-drawer': drawerVisible,
          'is-closing': drawerClosing,
          'is-published-preview': isPublishedView,
          'is-draft-preview': !isPublishedView && !isEditing,
        }"
      >
        <div v-if="!drawerVisible" class="setup-content-step-title">
          <div class="setup-content-step-nav">
            <el-button circle :disabled="activeStep <= 0" @click="prevStep">
              <el-icon><ArrowLeft /></el-icon>
            </el-button>
            <el-button circle :disabled="activeStep >= steps.length - 1" @click="nextStep">
              <el-icon><ArrowRight /></el-icon>
            </el-button>
          </div>
          <span>{{ currentStepTitle }}</span>
          <div class="setup-content-step-actions">
            <template v-if="stepActionMode === 'edit'">
              <template v-if="(activeStep === 2 || activeStep === 4) && isEditing && !isPublishedView">
                <el-button
                  class="step-action-btn step-action-btn-cancel"
                  :disabled="drawerConfirmLoading || submitting"
                  @click="cancelEdit"
                >
                  取消
                </el-button>
                <el-button
                  class="step-action-btn"
                  type="primary"
                  :loading="drawerConfirmLoading || submitting"
                  :disabled="!canEditSetup"
                  @click="handleDrawerConfirm"
                >
                  保存
                </el-button>
              </template>
              <el-button v-else class="step-action-btn" type="primary" :disabled="!canEditSetup" @click="startEdit">编辑</el-button>
            </template>
            <template v-else-if="stepActionMode === 'project-milestone'">
              <el-dropdown trigger="click" @command="onProjectMilestoneTemplateSelect">
                <el-button class="step-action-btn" type="primary" :disabled="!canEditSetup">
                  <span>选择模板</span>
                </el-button>
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item
                      v-for="item in projectMilestoneTemplates"
                      :key="item.key"
                      :command="item.key"
                    >
                      {{ item.label }}
                    </el-dropdown-item>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
              <el-button class="step-action-btn" type="primary" :disabled="!canEditSetup" @click="handleAddProjectMilestoneAction">新增</el-button>
            </template>
            <template v-else-if="stepActionMode === 'site-milestone'">
              <el-dropdown trigger="click" @command="onSiteMilestoneTemplateSelect">
                <el-button class="step-action-btn" type="primary" :disabled="!canEditSetup">
                  <span>选择模板</span>
                </el-button>
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item
                      v-for="item in siteMilestoneTemplates"
                      :key="item.key"
                      :command="item.key"
                    >
                      {{ item.label }}
                    </el-dropdown-item>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
              <el-button class="step-action-btn" type="primary" :disabled="!canEditSetup" @click="handleAddSiteMilestoneAction">新增</el-button>
            </template>
          </div>
        </div>
        <div v-if="drawerVisible" class="setup-drawer-header">
          <div class="setup-drawer-title-wrap">
            <div class="setup-drawer-title">{{ currentStepTitle }}</div>
          </div>
        </div>

    <section v-show="activeStep === 0" class="setup-section">
      <template v-if="project">
        <el-tabs v-model="infoTab" class="setup-info-tabs">
          <el-tab-pane v-if="showStep1BasicTab" label="基本信息" name="basic">
            <el-form
              v-if="isEditing && !isPublishedView"
              ref="formRef"
              :model="form"
              :rules="rules"
              label-width="120px"
              class="detail-form"
              label-position="top"
            >
              <div v-if="canEditStep1BasicGroup('project')" class="basic-edit-group">
                <div class="basic-form-group-title">项目信息</div>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item :label="TEXT.common.fields.projectName" prop="name">
                      <el-input v-model="form.name" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="项目全称">
                      <el-input v-model="form.project_full_name" placeholder="项目全称" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="申办方">
                      <el-input v-model="form.sponsor" :placeholder="TEXT.modules.adminProjects.sponsorPlaceholder" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item :label="TEXT.common.fields.projectCode" prop="code">
                      <el-input v-model="form.code" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item :label="TEXT.common.fields.protocolNo" prop="protocol_no">
                      <el-input v-model="form.protocol_no" :placeholder="TEXT.modules.adminProjects.protocolPlaceholder" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="组长单位">
                      <el-input v-model="form.lead_unit" placeholder="组长单位" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="主要研究者">
                      <el-input v-model="form.principal_investigator" placeholder="主要研究者" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="主PM">
                      <el-input v-model="form.main_pm" placeholder="主PM" />
                    </el-form-item>
                  </el-col>
                </el-row>
              </div>

              <div v-if="canEditStep1BasicGroup('research')" class="basic-edit-group">
                <div class="basic-form-group-title">研究信息</div>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="研究分期">
                      <el-input v-model="form.research_analysis" placeholder="研究分期" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="研究产品">
                      <el-input v-model="form.research_product" placeholder="研究产品" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="对照产品">
                      <el-input v-model="form.control_product" placeholder="对照产品" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="适应症">
                      <el-input v-model="form.indication" placeholder="适应症" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="研究人群">
                      <el-input v-model="form.research_population" placeholder="研究人群" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="研究设计">
                      <el-input v-model="form.research_design" placeholder="研究设计" />
                    </el-form-item>
                  </el-col>
                </el-row>
              </div>

              <div v-if="canEditStep1BasicGroup('execution')" class="basic-edit-group">
                <div class="basic-form-group-title">执行信息</div>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="计划开始日期">
                      <el-date-picker v-model="form.plan_start_date" type="date" value-format="YYYY-MM-DD" class="w-full" />
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="计划结束日期">
                      <el-date-picker v-model="form.plan_end_date" type="date" value-format="YYYY-MM-DD" class="w-full" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item :label="TEXT.common.fields.status" prop="status">
                      <el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select" class="w-full">
                        <el-option :label="TEXT.enums.projectStatus.DRAFT" value="DRAFT" />
                        <el-option :label="TEXT.enums.projectStatus.ACTIVE" value="ACTIVE" />
                        <el-option :label="TEXT.enums.projectStatus.CLOSED" value="CLOSED" />
                      </el-select>
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="计划中心数">
                      <el-input-number v-model="form.planned_site_count" :min="0" :step="1" controls-position="right" class="w-full" />
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row :gutter="24">
                  <el-col :span="12">
                    <el-form-item label="计划入组数">
                      <el-input-number v-model="form.planned_enrollment_count" :min="0" :step="1" controls-position="right" class="w-full" />
                    </el-form-item>
                  </el-col>
                </el-row>
              </div>
            </el-form>

            <div v-else class="basic-info-groups">
              <div class="info-group">
                <div class="info-group-title-row">
                  <div class="info-group-title">项目信息</div>
                  <el-button
                    v-if="!isPublishedView"
                    type="primary"
                    class="info-group-edit-btn"
                    :disabled="!canEditSetup"
                    @click="startStep1SectionEdit('project')"
                  >
                    编辑
                  </el-button>
                </div>
                <div class="info-grid">
                  <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-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>
              <div class="info-group">
                <div class="info-group-title-row">
                  <div class="info-group-title">研究信息</div>
                  <el-button
                    v-if="!isPublishedView"
                    type="primary"
                    class="info-group-edit-btn"
                    :disabled="!canEditSetup"
                    @click="startStep1SectionEdit('research')"
                  >
                    编辑
                  </el-button>
                </div>
                <div class="info-grid">
                  <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-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>
              <div class="info-group">
                <div class="info-group-title-row">
                  <div class="info-group-title">执行信息</div>
                  <el-button
                    v-if="!isPublishedView"
                    type="primary"
                    class="info-group-edit-btn"
                    :disabled="!canEditSetup"
                    @click="startStep1SectionEdit('execution')"
                  >
                    编辑
                  </el-button>
                </div>
                <div class="info-grid">
                  <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-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>
            </div>
          </el-tab-pane>

          <el-tab-pane v-if="showStep1SummaryTab" label="方案摘要" name="summary">
            <div class="info-group summary-display-group">
              <div class="info-group-title-row">
                <div class="info-group-title">访视计划</div>
                <div class="info-group-title-actions">
                  <el-tag
                    v-if="isProjectSnapshotFieldUpdated('visit_schedule') && shouldShowPreviewUpdateMark"
                    size="small"
                    type="warning"
                    effect="plain"
                    class="field-updated-tag"
                  >
                    已更新
                  </el-tag>
                  <el-button
                    v-if="!isPublishedView"
                    type="primary"
                    class="info-group-edit-btn"
                    :disabled="!canEditSetup"
                    @click="startStep1SectionEdit('summary')"
                  >
                    编辑
                  </el-button>
                </div>
              </div>
              <div class="visit-schedule-display">
                <div class="visit-schedule-display-head">
                  <span>访视</span>
                  <span>基线后天数</span>
                  <span>访视窗</span>
                </div>
                <div
                  v-for="(row, index) in summaryVisitScheduleRows"
                  :key="`${row.visit_code || 'visit'}-${index}`"
                  class="visit-schedule-display-row"
                >
                  <span class="visit-schedule-display-code">{{ row.visit_code || "-" }}</span>
                  <span>{{ row.baseline_offset_days }} 天</span>
                  <span>{{ formatVisitWindow(row) }}</span>
                </div>
                <div v-if="!summaryVisitScheduleRows.length" class="visit-schedule-display-empty">暂无访视计划</div>
              </div>
            </div>
          </el-tab-pane>
        </el-tabs>
      </template>

      <StateLoading v-else :rows="4" />
      </section>

    <section v-show="activeStep === 1" class="setup-section setup-section--table">
      <el-table :data="currentSetupDraft.projectMilestones" class="ctms-table setup-milestone-table">
        <el-table-column label="里程碑" min-width="180">
          <template #default="scope">
            <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">
          <template #default="scope">
            <div class="milestone-plan-cell">
              <div class="milestone-plan-line">
                <span class="milestone-plan-dot" />
                <span class="milestone-plan-label">开始:</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 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 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="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="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="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">
          <template #default="scope">
            <div class="milestone-actions-inline">
              <el-button link type="primary" :disabled="!canEditSetup" @click="openProjectMilestoneEditor(scope.$index)">编辑</el-button>
              <el-button link type="danger" :disabled="!canEditSetup" @click="removeProjectMilestone(scope.$index)">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>
      </section>

    <section v-show="activeStep === 2" class="setup-section setup-section--table enrollment-plan-section">
      <div class="enrollment-plan-toolbar">
        <div class="enrollment-inline-group">
          <span class="enrollment-inline-label required">计划入组日期</span>
          <div class="field-cell enrollment-date-cell" data-field-path="enrollmentPlan.startDate">
            <el-date-picker
              v-if="isEditing && !isPublishedView"
              v-model="currentSetupDraft.enrollmentPlan.startDate"
              type="date"
              value-format="YYYY-MM-DD"
              :disabled="!canEditSetup"
              class="w-full"
            />
            <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>
          <div class="field-cell enrollment-date-cell" data-field-path="enrollmentPlan.endDate">
            <el-date-picker
              v-if="isEditing && !isPublishedView"
              v-model="currentSetupDraft.enrollmentPlan.endDate"
              type="date"
              value-format="YYYY-MM-DD"
              :disabled="!canEditSetup"
              class="w-full"
            />
            <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>
        <div class="enrollment-inline-group">
          <span class="enrollment-inline-label">入组周期</span>
          <el-radio-group
            v-if="isEditing && !isPublishedView"
            v-model="enrollmentPlanCycle"
            size="small"
            class="enrollment-cycle-group"
          >
            <el-radio-button label="month">月</el-radio-button>
            <el-radio-button label="quarter">季度</el-radio-button>
          </el-radio-group>
          <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>
          <div class="field-cell enrollment-target-cell" data-field-path="enrollmentPlan.totalTarget">
            <el-input-number
              v-if="isEditing && !isPublishedView"
              v-model="currentSetupDraft.enrollmentPlan.totalTarget"
              :min="0"
              :disabled="!canEditSetup"
              controls-position="right"
              class="w-full"
            />
            <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>
      </div>

      <div class="enrollment-plan-summary">
        <div class="enrollment-summary-line">
          <span class="summary-title">项目计划情况:</span>
          <span>要求试验参与者数 <b>{{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }}</b> 人</span>
          <span>已填写计划数 <b>{{ toDisplayNumber(projectMonthlyPlannedCount) }}</b> 人</span>
          <span v-if="projectMonthlyOverflowCount > 0" class="summary-danger">已超出 {{ projectMonthlyOverflowCount }} 人</span>
          <span v-else>待分配 {{ projectMonthlyPendingCount }} 人</span>
        </div>
        <div class="enrollment-summary-line">
          <span class="summary-title">中心计划情况:</span>
          <span>已配置中心 <b>{{ centerConfiguredSiteCount }}</b>/<b>{{ centerActiveSiteCount }}</b> 个</span>
          <span>中心计划总数 <b>{{ toDisplayNumber(centerPlannedCount) }}</b> 人</span>
          <span>已填写计划数 <b>{{ toDisplayNumber(centerCyclePlannedCount) }}</b> 人</span>
          <span v-if="centerCycleOverflowCount > 0" class="summary-danger">已超出 {{ centerCycleOverflowCount }} 人</span>
          <span v-else-if="centerCyclePendingCount > 0">待分配 {{ centerCyclePendingCount }} 人</span>
          <span v-else class="summary-success">已完成分配</span>
        </div>
      </div>

      <div v-if="isEditing && !isPublishedView" class="enrollment-plan-actions">
        <el-button link class="enrollment-plan-action-link" @click="clearEnrollmentPeriodPlan">清空</el-button>
        <el-button link class="enrollment-plan-action-link" @click="quickFillEnrollmentPeriodPlan">快速填写</el-button>
      </div>

      <el-table
        v-if="currentEnrollmentPlanCycle === 'month'"
        :data="enrollmentPlanYearRows"
        class="ctms-table enrollment-month-plan-table"
        border
        empty-text="请先填写有效的计划入组日期范围"
      >
        <el-table-column label="年份" width="88">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.year }}</span>
          </template>
        </el-table-column>
        <el-table-column v-for="month in 12" :key="month" :label="`${month}月`" min-width="88">
          <template #default="scope">
            <template v-if="scope.row.cells[month - 1].key">
              <el-input-number
                v-if="isEditing && !isPublishedView"
                :model-value="getEnrollmentPeriodTarget(scope.row.cells[month - 1].key)"
                :min="0"
                :step="1"
                controls-position="right"
                class="month-target-input"
                @update:model-value="setEnrollmentPeriodTarget(scope.row.cells[month - 1].key, $event as number | null)"
              />
              <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>
        </el-table-column>
      </el-table>

      <el-table
        v-else
        :data="enrollmentCycleGridRows"
        class="ctms-table enrollment-period-plan-table"
        border
        empty-text="请先填写有效的计划入组日期范围"
      >
        <el-table-column :label="enrollmentCycleRowLabel" min-width="120">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.groupLabel }}</span>
          </template>
        </el-table-column>
        <el-table-column
          v-for="(column, columnIndex) in enrollmentCycleGridColumns"
          :key="column.key"
          :label="column.label"
          min-width="96"
        >
          <template #default="scope">
            <template v-if="scope.row.cells[columnIndex]?.key">
              <el-input-number
                v-if="isEditing && !isPublishedView"
                :model-value="getEnrollmentPeriodTarget(scope.row.cells[columnIndex].key)"
                :min="0"
                :step="1"
                controls-position="right"
                class="month-target-input"
                @update:model-value="setEnrollmentPeriodTarget(scope.row.cells[columnIndex].key, $event as number | null)"
              />
              <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>
        </el-table-column>
      </el-table>
      </section>

    <section v-show="activeStep === 3" class="setup-section setup-section--table">
      <el-table :data="currentSetupDraft.siteMilestones" class="ctms-table">
        <el-table-column label="里程碑" min-width="220">
          <template #default="scope">
            <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="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="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="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="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">
          <template #default="scope">
            <div class="milestone-actions-inline">
              <el-button link type="primary" :disabled="!canEditSetup" @click="openSiteMilestoneEditor(scope.$index)">编辑</el-button>
              <el-button link type="danger" :disabled="!canEditSetup" @click="removeSiteMilestone(scope.$index)">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>
      </section>

    <section v-show="activeStep === 4" class="setup-section setup-section--table enrollment-plan-section">
      <div class="enrollment-plan-toolbar">
        <div class="site-plan-selector">
          <span class="enrollment-inline-label">中心</span>
          <el-select
            v-model="selectedSiteEnrollmentPlanSiteId"
            filterable
            clearable
            placeholder="选择中心"
            class="site-plan-site-select"
          >
            <el-option
              v-for="site in siteEnrollmentSelectableSites"
              :key="normalizeSiteId(site.id)"
              :label="site.name || getSiteOptionLabel(site)"
              :value="normalizeSiteId(site.id)"
            />
          </el-select>
        </div>
        <div class="enrollment-inline-group">
          <span class="enrollment-inline-label required">中心入组日期</span>
          <div class="field-cell enrollment-date-cell">
            <el-date-picker
              v-if="isEditing && !isPublishedView && selectedSiteEnrollmentPlan"
              v-model="selectedSiteEnrollmentPlan.startDate"
              type="date"
              value-format="YYYY-MM-DD"
              :disabled="!canEditSetup"
              class="w-full"
            />
            <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">
            <el-date-picker
              v-if="isEditing && !isPublishedView && selectedSiteEnrollmentPlan"
              v-model="selectedSiteEnrollmentPlan.endDate"
              type="date"
              value-format="YYYY-MM-DD"
              :disabled="!canEditSetup"
              class="w-full"
            />
            <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">
          <span class="enrollment-inline-label">入组周期</span>
          <el-radio-group
            v-if="isEditing && !isPublishedView"
            v-model="enrollmentPlanCycle"
            size="small"
            class="enrollment-cycle-group"
          >
            <el-radio-button label="month">月</el-radio-button>
            <el-radio-button label="quarter">季度</el-radio-button>
          </el-radio-group>
          <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>
          <div class="field-cell enrollment-target-cell">
            <el-input-number
              v-if="isEditing && !isPublishedView && selectedSiteEnrollmentPlan"
              v-model="selectedSiteEnrollmentPlan.target"
              :min="0"
              :disabled="!canEditSetup"
              controls-position="right"
              class="w-full"
            />
            <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>

      <div class="enrollment-plan-summary">
        <div class="enrollment-summary-line">
          <span class="summary-title">项目计划情况:</span>
          <span>要求试验参与者数 <b>{{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }}</b> 人</span>
          <span>已填写计划数 <b>{{ toDisplayNumber(projectMonthlyPlannedCount) }}</b> 人</span>
          <span v-if="projectMonthlyOverflowCount > 0" class="summary-danger">已超出 {{ projectMonthlyOverflowCount }} 人</span>
          <span v-else>待分配 {{ projectMonthlyPendingCount }} 人</span>
        </div>
        <div class="enrollment-summary-line">
          <span class="summary-title">中心计划情况:</span>
          <span class="summary-site-name">{{ selectedSitePlanName }}</span>
          <span>计划入组数 <b>{{ toDisplayNumber(selectedSitePlanTarget) }}</b> 人</span>
          <span>已填写计划数 <b>{{ toDisplayNumber(selectedSiteCyclePlannedCount) }}</b> 人</span>
          <span>待分配 {{ selectedSiteCyclePendingCount }} 人</span>
        </div>
      </div>

      <div class="enrollment-plan-actions">
        <template v-if="isEditing && !isPublishedView">
          <el-button
            v-if="!selectedSiteEnrollmentPlan"
            link
            class="enrollment-plan-action-link"
            @click="createSelectedSiteEnrollmentPlan"
          >
            初始化当前中心计划
          </el-button>
          <template v-else>
            <el-button link class="enrollment-plan-action-link" @click="clearSiteEnrollmentPeriodPlan(selectedSiteEnrollmentPlanIndex)">清空</el-button>
            <el-button link class="enrollment-plan-action-link" @click="quickFillSiteEnrollmentPeriodPlan(selectedSiteEnrollmentPlanIndex)">快速填写</el-button>
            <el-button link type="danger" @click="removeSiteEnrollment(selectedSiteEnrollmentPlanIndex)">删除</el-button>
          </template>
        </template>
      </div>

      <div v-if="!selectedSiteEnrollmentPlanSiteId" class="month-empty">请先选择中心</div>
      <div v-else-if="!selectedSiteEnrollmentPlan" class="month-empty">当前中心暂未配置入组计划</div>
      <div v-else>
        <el-table
          v-if="currentEnrollmentPlanCycle === 'month'"
          :data="enrollmentPlanYearRows"
          class="ctms-table enrollment-month-plan-table"
          border
          empty-text="请先填写有效的计划入组日期范围"
        >
          <el-table-column label="年份" width="88">
            <template #default="scope">
              <span class="milestone-cell-text">{{ scope.row.year }}</span>
            </template>
          </el-table-column>
          <el-table-column v-for="month in 12" :key="month" :label="`${month}月`" min-width="88">
            <template #default="scope">
              <template v-if="scope.row.cells[month - 1].key">
                <el-input-number
                  v-if="isEditing && !isPublishedView"
                  :model-value="getSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[month - 1].key)"
                  :min="0"
                  :step="1"
                  controls-position="right"
                  class="month-target-input"
                  @update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[month - 1].key, $event as number | null)"
                />
                <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>
          </el-table-column>
        </el-table>

        <el-table
          v-else
          :data="enrollmentCycleGridRows"
          class="ctms-table enrollment-period-plan-table"
          border
          empty-text="请先填写有效的计划入组日期范围"
        >
          <el-table-column :label="enrollmentCycleRowLabel" min-width="120">
            <template #default="scope">
              <span class="milestone-cell-text">{{ scope.row.groupLabel }}</span>
            </template>
          </el-table-column>
          <el-table-column
            v-for="(column, columnIndex) in enrollmentCycleGridColumns"
            :key="column.key"
            :label="column.label"
            min-width="96"
          >
            <template #default="scope">
              <template v-if="scope.row.cells[columnIndex]?.key">
                <el-input-number
                  v-if="isEditing && !isPublishedView"
                  :model-value="getSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[columnIndex].key)"
                  :min="0"
                  :step="1"
                  controls-position="right"
                  class="month-target-input"
                  @update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[columnIndex].key, $event as number | null)"
                />
                <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>
          </el-table-column>
        </el-table>
      </div>
    </section>

    <section v-show="activeStep === 5" class="setup-section setup-section--table">
      <el-table :data="centerConfirmDisplayRows" class="ctms-table">
        <el-table-column label="中心" min-width="180">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.siteName || "-" }}</span>
          </template>
        </el-table-column>
        <el-table-column label="城市" min-width="120">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.city || "-" }}</span>
          </template>
        </el-table-column>
        <el-table-column label="PI" min-width="120">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.piName || "-" }}</span>
          </template>
        </el-table-column>
        <el-table-column label="负责人" min-width="180">
          <template #default="scope">
            <span class="milestone-cell-text">{{ scope.row.ownerDisplay || "-" }}</span>
          </template>
        </el-table-column>
        <el-table-column label="计划入组数" width="140">
          <template #default="scope">
            <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">
          <template #default="scope">
            <el-tag :type="scope.row.isActive ? 'success' : 'danger'" effect="light">
              {{ scope.row.isActive ? "激活" : "锁定" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="入组开始时间" width="150">
          <template #default="scope">
            <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="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>
      </section>
      <div v-if="drawerVisible" class="setup-drawer-footer">
        <el-button @click="closeEditDrawer">取消</el-button>
        <el-button type="primary" :loading="drawerConfirmLoading || submitting" :disabled="!canEditSetup" @click="handleDrawerConfirm">
          保存
        </el-button>
      </div>
      </div>
    </div>
    <el-dialog v-if="publishConfirmVisible" append-to="body" v-model="publishConfirmVisible" title="发布确认" width="920px" top="8vh" class="setup-publish-dialog">
      <div class="conflict-summary">
        <div>发布目标：立项配置草稿</div>
        <div>当前发布版本：{{ setupPublishedVersionText }}</div>
      </div>
      <div v-if="incompleteSteps.length" class="publish-incomplete">
        <el-alert
          type="warning"
          :closable="false"
          show-icon
          :title="`仍有 ${incompleteSteps.length} 个步骤待完善，建议补全后再发布`"
        />
        <div class="publish-incomplete-list">
          <el-tag v-for="item in incompleteSteps" :key="item.index" type="warning" effect="plain">
            {{ item.index + 1 }}. {{ item.label }}
          </el-tag>
        </div>
      </div>
      <el-alert
        v-if="isFirstPublish"
        type="info"
        :closable="false"
        show-icon
        title="当前为首次发布，将以当前草稿生成发布快照。"
      />
      <div v-else-if="publishDiffLines.length" class="conflict-diff-lines">
        <el-tag v-for="line in publishDiffLines" :key="line" effect="plain" type="warning">{{ line }}</el-tag>
      </div>
      <div v-if="publishFieldDiffRows.length" class="conflict-readable-diff">
        <div class="publish-diff-overview">
          <el-tag v-for="item in publishDiffModuleSummary" :key="item.module" type="info" effect="plain">
            {{ item.module }}：{{ item.count }} 项变更
          </el-tag>
        </div>
        <el-table :data="publishFieldDiffRows" size="small" border max-height="320">
          <el-table-column prop="moduleLabel" label="模块" width="140" />
          <el-table-column prop="path" label="变更项" min-width="300" />
          <el-table-column prop="changeType" label="变更类型" width="100">
            <template #default="scope">
              <el-tag :type="scope.row.changeType === '新增' ? 'success' : scope.row.changeType === '删除' ? 'danger' : 'warning'" effect="plain">
                {{ scope.row.changeType }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="serverValue" label="上一版本值" min-width="220" show-overflow-tooltip />
          <el-table-column prop="localValue" label="预发布版本值" min-width="220" show-overflow-tooltip />
        </el-table>
      </div>
      <el-alert
        v-else-if="!isFirstPublish"
        type="info"
        :closable="false"
        show-icon
        title="暂未检测到可展示的差异明细。"
      />
      <template #footer>
        <el-button @click="publishConfirmVisible = false">取消</el-button>
        <el-button type="primary" :loading="publishConfirmLoading" @click="confirmPublishConfig">确认发布</el-button>
      </template>
    </el-dialog>
    <el-dialog
      v-if="rollbackDialogVisible"
      append-to="body"
      v-model="rollbackDialogVisible"
      width="960px"
      top="6vh"
      :show-close="false"
      class="version-mgr-dialog"
    >
      <template #header>
        <div class="vm-dialog-header">
          <div class="vm-dialog-header-left">
            <span class="vm-dialog-icon">
              <el-icon :size="20"><List /></el-icon>
            </span>
            <span class="vm-dialog-title">版本管理</span>
            <el-tag class="vm-version-count-tag" size="small" effect="plain" round>
              {{ rollbackAxisRows.length }} 个版本
            </el-tag>
          </div>
          <el-button class="vm-dialog-close-btn" :icon="Close" circle size="small" @click="rollbackDialogVisible = false" />
        </div>
      </template>

      <div class="vm-body" v-loading="rollbackDialogLoading">
        <!-- 分支轨道图例 -->
        <div class="vm-legend-bar">
          <div class="vm-legend-section">
            <span class="vm-legend-label">分支轨道</span>
            <div class="vm-legend-tags">
              <span
                v-for="lane in rollbackAxisLanes"
                :key="lane.branch"
                class="vm-lane-badge"
                :class="{ 'is-main': lane.branch === 'main' }"
              >
                <span class="vm-lane-badge-dot" />
                {{ lane.branch }}
              </span>
            </div>
          </div>
        </div>

        <!-- 轨道头部标签 -->
        <div class="rollback-axis-lane-head" :style="{ width: `${rollbackAxisWidth}px` }">
          <span
            v-for="lane in rollbackAxisLanes"
            :key="`head-${lane.branch}`"
            class="rollback-axis-lane-head-item"
            :class="{ 'is-main': lane.branch === 'main' }"
            :style="getRollbackAxisLeftStyle(lane.x)"
          >
            {{ formatAxisLaneLabel(lane.branch) }}
          </span>
        </div>

        <!-- 时间线内容 -->
        <div class="rollback-axis-scroll" ref="rollbackAxisScrollRef">
          <svg
            v-if="rollbackAxisOverlayHeight > 0"
            class="rollback-axis-overlay"
            :width="rollbackAxisWidth"
            :height="rollbackAxisOverlayHeight"
            :viewBox="`0 0 ${rollbackAxisWidth} ${rollbackAxisOverlayHeight}`"
            preserveAspectRatio="none"
          >
            <defs>
              <linearGradient id="mergeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="#f59e0b" stop-opacity="0.8" />
                <stop offset="100%" stop-color="#ef4444" stop-opacity="0.6" />
              </linearGradient>
              <linearGradient id="spawnGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="#3b82f6" stop-opacity="0.7" />
                <stop offset="100%" stop-color="#8b5cf6" stop-opacity="0.6" />
              </linearGradient>
            </defs>
            <!-- 同分支继承连接线 -->
            <g v-for="connector in rollbackAxisSameBranchConnectors" :key="connector.key">
              <line
                :class="connector.isMain ? 'rollback-axis-branch-line is-main' : 'rollback-axis-branch-line'"
                :x1="connector.x"
                :y1="connector.y1"
                :x2="connector.x"
                :y2="connector.y2"
              />
            </g>
            <!-- 合并连接线 -->
            <g v-for="connector in rollbackAxisMergeConnectors" :key="connector.key">
              <path
                class="rollback-axis-merge-path"
                :d="`M ${connector.x1} ${connector.y1} C ${connector.x1} ${(connector.y1 + connector.y2) / 2}, ${connector.x2} ${(connector.y1 + connector.y2) / 2}, ${connector.x2} ${connector.y2}`"
              />
            </g>
            <!-- 派生连接线 -->
            <g v-for="connector in rollbackAxisSpawnConnectors" :key="connector.key">
              <path
                class="rollback-axis-spawn-path"
                :d="`M ${connector.x1} ${connector.y1} C ${connector.x1} ${(connector.y1 + connector.y2) / 2}, ${connector.x2} ${(connector.y1 + connector.y2) / 2}, ${connector.x2} ${connector.y2}`"
              />
            </g>
          </svg>

          <div
            v-for="row in rollbackAxisRows"
            :key="row.id"
            class="rollback-axis-row"
            :class="{
              'is-current-row': row.is_current_published,
              'is-draft-base-row': row.is_active_draft_base,
            }"
            :ref="(el) => setRollbackAxisRowRef(el as HTMLElement | null, row.id)"
          >
            <div class="rollback-axis-graph" :style="{ width: `${rollbackAxisWidth}px` }">
              <span
                v-for="lane in rollbackAxisLanes.filter(l => l.branch === 'main')"
                :key="`${row.id}-${lane.branch}`"
                class="rollback-axis-lane-segment is-main-lane"
                :style="getRollbackAxisLeftStyle(lane.x)"
              />
              <span
                class="rollback-axis-dot"
                :class="{
                  'is-main': row.branch_name === 'main',
                  'is-current': row.is_current_published,
                  'is-merge': row.mergeFromId !== null,
                }"
                :style="getRollbackAxisLeftStyle(row.axisX)"
              />
            </div>
            <div class="rollback-axis-content">
              <div class="rollback-axis-topline">
                <span class="rollback-axis-version">{{ row.displayLabel }}</span>
                <span v-if="row.is_current_published" class="vm-status-badge vm-badge-published">
                  <el-icon :size="12"><SuccessFilled /></el-icon>
                  当前发布
                </span>
                <span v-if="row.is_active_draft_base" class="vm-status-badge vm-badge-draft">
                  <el-icon :size="12"><EditPen /></el-icon>
                  草稿基线
                </span>
                <span v-if="row.mergeFromLabel" class="vm-status-badge vm-badge-merge">
                  <el-icon :size="12"><Connection /></el-icon>
                  来源 {{ row.mergeFromLabel }}
                </span>
              </div>
              <div class="rollback-axis-meta">
                <span class="vm-meta-item">
                  <el-icon :size="13"><FolderOpened /></el-icon>
                  {{ row.branch_name || "main" }}
                </span>
                <span class="vm-meta-item">
                  <el-icon :size="13"><User /></el-icon>
                  {{ row.published_by_name || "-" }}
                </span>
                <span class="vm-meta-item">
                  <el-icon :size="13"><Clock /></el-icon>
                  {{ row.published_at }}
                </span>
              </div>
              <div class="rollback-axis-actions">
                <el-tooltip content="回滚到此版本" placement="top" :show-after="300">
                  <el-button class="vm-action-btn vm-action-danger" size="small" @click="selectRollbackVersion(row.version)">
                    <el-icon :size="13"><RefreshLeft /></el-icon>
                    回滚
                  </el-button>
                </el-tooltip>
                <el-tooltip v-if="(row.branch_name || 'main') === 'main'" content="基于此版本新建分支草稿" placement="top" :show-after="300">
                  <el-button class="vm-action-btn vm-action-primary" size="small" @click="checkoutBranchDraftFromVersion(row.version)">
                    <el-icon :size="13"><DocumentCopy /></el-icon>
                    新建分支草稿
                  </el-button>
                </el-tooltip>
                <el-tooltip v-if="(row.branch_name || 'main') !== 'main'" content="合并至主线创建新版本" placement="top" :show-after="300">
                  <el-button class="vm-action-btn vm-action-primary" size="small" @click="mergeVersionToMain(row.version)">
                    <el-icon :size="13"><Connection /></el-icon>
                    合并主线（新版本）
                  </el-button>
                </el-tooltip>
                <el-tooltip content="下载版本数据" placement="top" :show-after="300">
                  <el-button class="vm-action-btn vm-action-default" size="small" @click="downloadVersionRaw(row)">
                    <el-icon :size="13"><Download /></el-icon>
                    下载
                  </el-button>
                </el-tooltip>
                <el-tooltip content="删除此版本" placement="top" :show-after="300">
                  <el-button
                    class="vm-action-btn vm-action-danger"
                    size="small"
                    :disabled="isLatestPublishedVersion(row.version)"
                    @click="removeVersion(row.version)"
                  >
                    <el-icon :size="13"><Delete /></el-icon>
                    删除
                  </el-button>
                </el-tooltip>
              </div>
            </div>
          </div>
        </div>
      </div>

      <template #footer>
        <div class="vm-dialog-footer">
          <el-button round @click="rollbackDialogVisible = false">
            <el-icon><Close /></el-icon>
            关闭
          </el-button>
        </div>
      </template>
    </el-dialog>

    <el-dialog
      v-model="visitScheduleDialogVisible"
      title="编辑访视计划"
      width="min(860px, calc(100vw - 32px))"
      :close-on-click-modal="false"
      :show-close="false"
      class="visit-schedule-dialog"
    >
      <el-form label-width="120px" class="detail-form visit-schedule-dialog-form" label-position="top">
        <div class="summary-section-bar">
          <el-button plain size="small" @click="addVisitScheduleRow">添加访视</el-button>
        </div>
          <div class="visit-schedule-editor">
            <div class="visit-schedule-head">
            <span>访视</span>
            <span>基线后天数</span>
            <span>窗口前（天）</span>
            <span>窗口后（天）</span>
            <span />
          </div>
          <div
            v-for="(row, index) in form.visit_schedule"
            :key="`${row.visit_code || 'visit'}-${index}`"
            class="visit-schedule-row"
            :class="{
              'is-dragging': visitScheduleDragIndex === index,
              'insert-before': isVisitScheduleInsertTarget(index, 'before'),
              'insert-after': isVisitScheduleInsertTarget(index, 'after'),
            }"
            draggable="true"
            aria-label="拖拽调整访视顺序"
            @dragstart="handleVisitScheduleDragStart(index, $event)"
            @dragover.prevent="handleVisitScheduleDragOver(index, $event)"
            @drop.prevent="handleVisitScheduleDrop(index)"
            @dragenter.prevent="handleVisitScheduleDragOver(index, $event)"
            @dragend="handleVisitScheduleDragEnd"
          >
            <div class="visit-field visit-code-field">
              <span class="visit-field-label">访视</span>
              <el-select
                v-model="row.visit_code"
                filterable
                allow-create
                default-first-option
                placeholder="选择或输入访视"
                class="w-full"
                @change="handleVisitScheduleCodeChange(index, $event)"
              >
                <el-option-group label="常规访视">
                  <el-option label="常规访视（自动生成 Vn）" :value="regularVisitOptionValue" />
                  <el-option label="安全性随访（自动生成 Vn）" :value="safetyFollowUpVisitOptionValue" />
                </el-option-group>
                <el-option-group label="计划特殊访视">
                  <el-option
                    v-for="option in specialVisitOptions"
                    :key="option"
                    :label="option"
                    :value="option"
                  />
                </el-option-group>
              </el-select>
            </div>
            <div class="visit-field">
              <span class="visit-field-label">基线后天数</span>
              <el-input-number v-model="row.baseline_offset_days" :min="0" :max="3650" :step="1" controls-position="right" class="w-full" />
            </div>
            <div class="visit-field">
              <span class="visit-field-label">窗口前</span>
              <el-input-number v-model="row.window_before_days" :min="0" :max="365" :step="1" controls-position="right" class="w-full" />
            </div>
            <div class="visit-field">
              <span class="visit-field-label">窗口后</span>
              <el-input-number v-model="row.window_after_days" :min="0" :max="365" :step="1" controls-position="right" class="w-full" />
            </div>
            <el-button
              :icon="Delete"
              text
              type="danger"
              class="visit-schedule-remove"
              @click="removeVisitScheduleRow(index)"
            />
            </div>
        </div>
        <div class="visit-special-rule">
          <div class="visit-special-rule-title">特殊访视规则</div>
          <div class="visit-special-rule-body">
            提前终止不作为常规计划访视配置；受试者发生提前终止时，在访视页通过“提前终止”单独录入，系统会自动关闭后续未完成计划访视。
          </div>
        </div>
      </el-form>
      <template #footer>
        <div class="visit-schedule-dialog-footer">
          <el-button :disabled="drawerConfirmLoading || submitting" @click="cancelVisitScheduleDialogEdit">取消</el-button>
          <el-button type="primary" :loading="drawerConfirmLoading || submitting" :disabled="!canEditSetup" @click="saveVisitScheduleDialogEdit">
            保存
          </el-button>
        </div>
      </template>
    </el-dialog>

    <el-drawer
      v-if="projectMilestoneEditorVisible"
      v-model="projectMilestoneEditorVisible"
      direction="rtl"
      size="480px"
      :close-on-click-modal="false"
      :show-close="false"
      class="setup-milestone-editor-drawer"
    >
      <template #header>
        <div class="sme-header">
          <div class="sme-header-title">编辑里程碑</div>
          <div class="sme-header-subtitle">配置里程碑时间计划与负责人</div>
        </div>
      </template>
      <el-form label-position="top" class="sme-form">
        <!-- 基本信息 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-blue"></span>基本信息</div>
          <el-form-item label="里程碑名称">
            <el-input v-model="projectMilestoneEditorForm.name" placeholder="例如：首例入组" />
          </el-form-item>
        </div>
        <!-- 时间计划 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-amber"></span>时间计划</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="开始日期">
                <el-date-picker
                  v-model="projectMilestoneEditorForm.startDate"
                  type="date"
                  value-format="YYYY-MM-DD"
                  class="w-full"
                  placeholder="选择日期"
                />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="结束日期">
                <el-date-picker
                  v-model="projectMilestoneEditorForm.endDate"
                  type="date"
                  value-format="YYYY-MM-DD"
                  class="w-full"
                  placeholder="选择日期"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <div class="sme-duration-line">
            <span class="sme-duration-label">耗时</span>
            <span class="sme-duration-value">{{ formatProjectMilestoneDuration(projectMilestoneEditorForm) }}</span>
          </div>
        </div>
        <!-- 负责与状态 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-green"></span>负责与状态</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="负责人">
                <el-select
                  v-model="projectMilestoneEditorForm.owner"
                  filterable
                  clearable
                  class="w-full"
                  placeholder="选择负责人"
                >
                  <el-option
                    v-for="option in projectMilestoneOwnerOptions"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="状态">
                <el-select v-model="projectMilestoneEditorForm.status" class="w-full" placeholder="状态">
                  <el-option label="未开始" value="未开始" />
                  <el-option label="进行中" value="进行中" />
                  <el-option label="已完成" value="已完成" />
                  <el-option label="延期" value="延期" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- 备注 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-gray"></span>备注</div>
          <el-input v-model="projectMilestoneEditorForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
        </div>
      </el-form>
      <template #footer>
        <div class="sme-footer">
          <el-button @click="projectMilestoneEditorVisible = false">取消</el-button>
          <el-button type="primary" @click="saveProjectMilestoneEditor">保存</el-button>
        </div>
      </template>
    </el-drawer>

    <el-drawer
      v-if="siteMilestoneEditorVisible"
      v-model="siteMilestoneEditorVisible"
      direction="rtl"
      size="480px"
      :close-on-click-modal="false"
      :show-close="false"
      class="setup-milestone-editor-drawer"
    >
      <template #header>
        <div class="sme-header">
          <div class="sme-header-title">编辑中心里程碑</div>
          <div class="sme-header-subtitle">配置中心里程碑计划日期与负责人</div>
        </div>
      </template>
      <el-form label-position="top" class="sme-form">
        <!-- 基本信息 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-blue"></span>基本信息</div>
          <el-form-item label="里程碑名称">
            <el-input v-model="siteMilestoneEditorForm.milestone" placeholder="中心里程碑" />
          </el-form-item>
          <el-form-item label="计划日期">
            <el-date-picker
              v-model="siteMilestoneEditorForm.planDate"
              type="date"
              value-format="YYYY-MM-DD"
              class="w-full"
              placeholder="选择日期"
            />
          </el-form-item>
        </div>
        <!-- 负责与状态 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-green"></span>负责与状态</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="负责人">
                <el-select
                  v-model="siteMilestoneEditorForm.owner"
                  filterable
                  clearable
                  class="w-full"
                  placeholder="选择负责人"
                >
                  <el-option
                    v-for="option in siteMilestoneOwnerOptions"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="状态">
                <el-select v-model="siteMilestoneEditorForm.status" class="w-full" placeholder="状态">
                  <el-option label="未开始" value="未开始" />
                  <el-option label="进行中" value="进行中" />
                  <el-option label="已完成" value="已完成" />
                  <el-option label="延期" value="延期" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- 备注 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-gray"></span>备注</div>
          <el-input v-model="siteMilestoneEditorForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
        </div>
      </el-form>
      <template #footer>
        <div class="sme-footer">
          <el-button @click="siteMilestoneEditorVisible = false">取消</el-button>
          <el-button type="primary" @click="saveSiteMilestoneEditor">保存</el-button>
        </div>
      </template>
    </el-drawer>

    <el-drawer
      v-if="siteEnrollmentEditorVisible"
      v-model="siteEnrollmentEditorVisible"
      direction="rtl"
      size="480px"
      :close-on-click-modal="false"
      :show-close="false"
      class="setup-milestone-editor-drawer"
    >
      <template #header>
        <div class="sme-header">
          <div class="sme-header-title">编辑中心入组计划</div>
          <div class="sme-header-subtitle">配置中心入组目标与时间范围</div>
        </div>
      </template>
      <el-form label-position="top" class="sme-form">
        <!-- 中心选择 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-blue"></span>中心选择</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="中心">
                <el-select v-model="siteEnrollmentEditorForm.siteId" filterable clearable placeholder="选择中心" class="w-full">
                  <el-option
                    v-for="site in siteSelectOptions"
                    :key="site.id"
                    :label="site.label"
                    :value="site.id"
                    :disabled="isSiteEnrollmentSiteTaken(site.id)"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="计划例数">
                <el-input-number v-model="siteEnrollmentEditorForm.target" :min="0" controls-position="right" class="w-full" />
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- 时间计划 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-amber"></span>时间计划</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="启动日期">
                <el-date-picker v-model="siteEnrollmentEditorForm.startDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="完成日期">
                <el-date-picker v-model="siteEnrollmentEditorForm.endDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- 备注 -->
        <div class="sme-group">
          <div class="sme-group-title"><span class="sme-dot sme-dot-gray"></span>备注</div>
          <el-input v-model="siteEnrollmentEditorForm.note" type="textarea" :rows="3" placeholder="请输入备注信息" />
        </div>
      </el-form>
      <template #footer>
        <div class="sme-footer">
          <el-button @click="siteEnrollmentEditorVisible = false">取消</el-button>
          <el-button type="primary" @click="saveSiteEnrollmentEditor">保存</el-button>
        </div>
      </template>
    </el-drawer>

  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Ref } from "vue";
import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import { ArrowLeft, ArrowRight, Promotion, User, OfficeBuilding, Lock, SuccessFilled, ArrowDown, Close, List, EditPen, Connection, FolderOpened, Clock, RefreshLeft, DocumentCopy, Download, Delete } from "@element-plus/icons-vue";
import {
  fetchStudyDetail,
} from "../../api/studies";
import { fetchMyApiEndpointPermissions } from "../../api/projectPermissions";
import { listMembers } from "../../api/members";
import { fetchSites } from "../../api/sites";
import type { Site, Study } from "../../types/api";
import type {
  CenterConfirmDraft,
  ProjectPublishSnapshot,
  ProjectMilestoneDraft,
  SetupConfigDraft,
  SiteEnrollmentPlanDraft,
  SiteMilestoneDraft,
  StudySetupConfigResponse,
  StudySetupConfigVersionItem,
  StudySetupConfigUpsertPayload,
  VisitScheduleItem,
} from "../../types/setupConfig";
import StateLoading from "../../components/StateLoading.vue";
import { useStudyStore } from "../../store/study";
import { useAuthStore } from "../../store/auth";
import { TEXT, requiredMessage } from "../../locales";
import { isSystemAdmin } from "../../utils/roles";
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
import { groupErrorsBySection, parseFieldPath, type SetupValidationError } from "../../utils/setupFieldLocator";
import { buildSetupReadableDiffRows, serializeDiffValue, type SetupDiffRow } from "../../utils/setupDiffRows";
import {
  SETUP_WORKFLOW_TAG_META,
  buildProjectDiffRows,
  buildSetupModuleDiffLines,
  hasProjectSnapshotDiff,
  resolveProjectPublishCompareBase,
  resolvePublishedProjectSnapshot,
  resolveSetupWorkflowStatus,
  shouldCaptureProjectCompareFallback,
  type DraftSyncStatus,
  type SetupWorkflowStatus,
  type SetupWorkflowTagMeta,
} from "../../utils/setupPublishWorkflow";
import { useSetupConfig } from "../../composables/useSetupConfig";

type SetupStepKey =
  | "project-info"
  | "project-milestone"
  | "project-enrollment"
  | "site-milestone"
  | "site-enrollment"
  | "site-confirm";
type Step1EditSection = "all" | "project" | "research" | "execution" | "summary";
type StepActionMode =
  | "none"
  | "edit"
  | "project-milestone"
  | "site-milestone"
;
type RollbackAxisLane = { branch: string; index: number; x: number };
type RollbackAxisRow = StudySetupConfigVersionItem & {
  displayLabel: string;
  mergeFromId: string | null;
  axisX: number;
  mergeFromLabel: string;
  spawnParentId: string | null;
};

const DRAFT_VERSION = 2;
const LOCAL_DRAFT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const SAVE_RETRY_DELAYS_MS = [800, 1600, 3200];
type LocalDraftEnvelope<T> = {
  version?: number;
  data?: T;
  savedAt?: string;
  savedAtMs?: number;
  ttlMs?: number;
  serverVersionAtCache?: number | null;
};
type LocalDraftLoadResult<T> = {
  data: T;
  savedAt: string;
  expired: boolean;
  conflict: boolean;
  serverVersionAtCache: number | null;
};

const steps: Array<{ key: SetupStepKey; label: string }> = [
  { key: "project-info", label: "项目信息" },
  { key: "project-milestone", label: "项目里程碑" },
  { key: "project-enrollment", label: "项目入组计划" },
  { key: "site-milestone", label: "中心里程碑" },
  { key: "site-enrollment", label: "中心入组计划" },
  { key: "site-confirm", label: TEXT.modules.adminProjects.setupStepCenterConfirm || "中心确认" },
];
const projectMilestoneTemplates: Array<{ key: string; label: string; milestones: string[] }> = [
  {
    key: "startup-default",
    label: "默认模板（立项关键节点）",
    milestones: [
      "中心可行性调研",
      "试验启动前研究者会",
      "方案定稿",
      "首家中心伦理通过",
      "首家中心启动",
      "首例参与者入组",
      "参与者入组5例",
      "参与者入组20例",
      "末例参与者出组",
      "数据库锁定",
      "临床试验总结",
    ],
  },
];
const siteMilestoneTemplates: Array<{ key: string; label: string; milestones: string[] }> = [
  {
    key: "site-default",
    label: "默认模板（中心关键节点）",
    milestones: [
      "中心调研",
      "机构立项",
      "取得伦理批件",
      "合同签署",
      "启动会",
      "首例入组",
      "末例入组",
      "末例访视",
      "数据库锁定",
      "中心关闭",
    ],
  },
];


const route = useRoute();
const router = useRouter();
const studyStore = useStudyStore();
const authStore = useAuthStore();
const {
  getConfig: fetchSetupConfig,
  saveConfig: upsertSetupConfig,
  publishConfig,
  mergeToMain: mergeSetupVersionToMain,
  listVersions: fetchSetupVersions,
  rollbackConfig,
  checkoutBranchDraft: checkoutSetupBranchDraft,
  clearDraft: clearSetupDraftOnServer,
  refillDraft: refillSetupDraftOnServer,
  deleteVersion: deleteSetupVersion,
} = useSetupConfig();

const project = ref<Study | null>(null);
const siteOptions = ref<Site[]>([]);
const memberDisplayMap = ref<Record<string, string>>({});
const projectMemberOptions = ref<Array<{ value: string; label: string }>>([]);
const activeStep = ref(0);
const infoTab = ref<"basic" | "summary">("basic");
const step1EditSection = ref<Step1EditSection>("all");
const isEditing = ref(false);
const drawerClosing = ref(false);
const visitScheduleDialogVisible = ref(false);
const drawerVisible = computed(() => {
  if (visitScheduleDialogVisible.value) return false;
  if (activeStep.value === 2 || activeStep.value === 4) return false;
  return isEditing.value || drawerClosing.value;
});
const visitScheduleDragIndex = ref<number | null>(null);
const visitScheduleDragOverIndex = ref<number | null>(null);
type VisitScheduleInsertPosition = "before" | "after";
const visitScheduleInsertPosition = ref<VisitScheduleInsertPosition>("before");
const drawerCloseTimer = ref<number | undefined>(undefined);
const submitting = ref(false);
const formRef = ref<FormInstance>();
const draftReady = ref(false);
const suppressDraftWatch = ref(false);
const autoSaveTimer = ref<number | undefined>(undefined);
const setupDirtySinceLastPersist = ref(false);
const suppressEnrollmentFieldSync = ref(false);
const enrollmentPlanCycle = ref<EnrollmentCycle>("month");
const enrollmentTargetsByCycle = ref<Record<EnrollmentCycle, Record<string, number>>>({
  month: {},
  quarter: {},
});
const enrollmentStageSyncingFromModel = ref(false);
const enrollmentStageSyncingToModel = ref(false);
const setupRevision = ref<number>(1);
const setupPublishedVersion = ref<string>("v0");
const setupPublishedBranchName = ref<string>("main");
const setupCurrentBranchName = ref<string>("main");
const setupActiveDraftBaseVersion = ref<string>("");
const setupPublishStatus = ref<"DRAFT" | "PUBLISHED" | string>("DRAFT");
const setupPublishedAt = ref<string>("");
type SetupViewMode = "draft" | "preview" | "published";
const setupViewMode = ref<SetupViewMode>("draft");
const publishedSetupDraft = ref<SetupConfigDraft | null>(null);
const publishConfirmVisible = ref(false);
const publishConfirmLoading = ref(false);
const primarySaveLoading = ref(false);
const rollbackDialogVisible = ref(false);
const rollbackDialogLoading = ref(false);
const setupVersionHistory = ref<StudySetupConfigVersionItem[]>([]);
const drawerConfirmLoading = ref(false);
const projectMilestoneEditorVisible = ref(false);
const projectMilestoneEditingIndex = ref<number>(-1);
const siteMilestoneEditorVisible = ref(false);
const siteMilestoneEditingIndex = ref<number>(-1);
const siteEnrollmentEditorVisible = ref(false);
const siteEnrollmentEditingIndex = ref<number>(-1);

type IndexedEditorController = {
  visible: Ref<boolean>;
  index: Ref<number>;
};
const projectMilestoneEditor: IndexedEditorController = {
  visible: projectMilestoneEditorVisible,
  index: projectMilestoneEditingIndex,
};
const siteMilestoneEditor: IndexedEditorController = {
  visible: siteMilestoneEditorVisible,
  index: siteMilestoneEditingIndex,
};
const siteEnrollmentEditor: IndexedEditorController = {
  visible: siteEnrollmentEditorVisible,
  index: siteEnrollmentEditingIndex,
};

const projectMilestoneEditorForm = reactive<ProjectMilestoneDraft>({
  id: "",
  name: "",
  planDate: "",
  startDate: "",
  endDate: "",
  durationDays: 1,
  owner: "",
  remark: "",
  status: "未开始",
});
const siteMilestoneEditorForm = reactive<SiteMilestoneDraft>({
  id: "",
  milestone: "",
  planDate: "",
  owner: "",
  remark: "",
  status: "未开始",
});
const siteEnrollmentEditorForm = reactive<SiteEnrollmentPlanDraft>({
  id: "",
  siteId: "",
  siteName: "",
  target: 0,
  startDate: "",
  endDate: "",
  note: "",
  stageBreakdown: "",
});


type EnrollmentCycle = "month" | "quarter";
type EnrollmentYearRow = {
  year: number;
  cells: Array<{ key: string | null }>;
};
type EnrollmentCycleGridRow = {
  groupLabel: string;
  cells: Array<{ key: string | null }>;
};
type EnrollmentCycleGridColumn = {
  key: string;
  label: string;
};
type EnrollmentPeriodSlot = {
  key: string;
  label: string;
  groupLabel: string;
};

const publishedProjectSnapshot = ref<ProjectPublishSnapshot | null>(null);
const projectPublishCompareFallback = ref<ProjectPublishSnapshot | null>(null);
const projectSnapshotAtLoad = ref<ProjectPublishSnapshot | null>(null);
const setupServerSnapshot = ref("");
const setupLocalDraftSavedAt = ref("");
const setupSaveMeta = ref<{ updatedAt: string; savedBy: string | null; serverSynced: boolean }>({
  updatedAt: "",
  savedBy: null,
  serverSynced: true,
});
const lastServerSaveMeta = ref<{ updatedAt: string; savedBy: string }>({
  updatedAt: "-",
  savedBy: "-",
});
const serverFieldErrors = ref<Record<string, string>>({});

const form = ref({
  code: "",
  name: "",
  project_full_name: "",
  short_name: "",
  sponsor: "",
  protocol_no: "",
  lead_unit: "",
  principal_investigator: "",
  main_pm: "",
  research_analysis: "",
  research_product: "",
  control_product: "",
  indication: "",
  research_population: "",
  research_design: "",
  plan_start_date: "",
  plan_end_date: "",
  planned_site_count: null as number | null,
  planned_enrollment_count: null as number | null,
  phase: "",
  status: "DRAFT",
  scope: "国内",
  visit_schedule: [] as VisitScheduleItem[],
});
const formBaselineSnapshot = ref("");
const regularVisitOptionValue = "__REGULAR_VISIT__";
const safetyFollowUpVisitOptionValue = "__SAFETY_FOLLOW_UP_VISIT__";
const specialVisitOptions = ["筛选访视", "基线访视", "终止治疗"];

const getNextRegularVisitCode = (excludeIndex?: number): string => {
  const maxRegularIndex = form.value.visit_schedule.reduce((maxIndex, row, rowIndex) => {
    if (rowIndex === excludeIndex) return maxIndex;
    const match = /^V(\d+)$/.exec((row.visit_code || "").trim());
    if (!match) return maxIndex;
    return Math.max(maxIndex, Number(match[1]));
  }, 0);
  return `V${maxRegularIndex + 1}`;
};

const getNextSafetyFollowUpVisitCode = (excludeIndex?: number): string => {
  const maxSafetyFollowUpIndex = form.value.visit_schedule.reduce((maxIndex, row, rowIndex) => {
    if (rowIndex === excludeIndex) return maxIndex;
    const match = /^安全性随访V(\d+)$/.exec((row.visit_code || "").trim());
    if (!match) return maxIndex;
    return Math.max(maxIndex, Number(match[1]));
  }, 0);
  return `安全性随访V${maxSafetyFollowUpIndex + 1}`;
};

const hasDuplicateVisitCode = (visitCode: string, currentIndex: number): boolean => {
  const normalizedCode = visitCode.trim();
  if (!normalizedCode) return false;
  return form.value.visit_schedule.some(
    (row, rowIndex) => rowIndex !== currentIndex && (row.visit_code || "").trim() === normalizedCode
  );
};

const normalizeVisitSchedule = (rows: VisitScheduleItem[] | undefined | null): VisitScheduleItem[] =>
  (rows || []).map((row) => ({
    visit_code: (row.visit_code || "").trim(),
    baseline_offset_days: Number(row.baseline_offset_days || 0),
    window_before_days: Number(row.window_before_days || 0),
    window_after_days: Number(row.window_after_days || 0),
  }));

const addVisitScheduleRow = () => {
  form.value.visit_schedule.push({
    visit_code: getNextRegularVisitCode(),
    baseline_offset_days: 0,
    window_before_days: 0,
    window_after_days: 0,
  });
};

const removeVisitScheduleRow = (index: number) => {
  form.value.visit_schedule.splice(index, 1);
};

const handleVisitScheduleCodeChange = (index: number, value: string) => {
  const row = form.value.visit_schedule[index];
  if (!row) return;
  let nextVisitCode = String(value || "").trim();
  if (nextVisitCode === regularVisitOptionValue) {
    nextVisitCode = getNextRegularVisitCode(index);
  } else if (nextVisitCode === safetyFollowUpVisitOptionValue) {
    nextVisitCode = getNextSafetyFollowUpVisitCode(index);
  }
  if (hasDuplicateVisitCode(nextVisitCode, index)) {
    row.visit_code = "";
    ElMessage.warning(`访视已存在：${nextVisitCode}`);
    return;
  }
  row.visit_code = nextVisitCode;
};

const handleVisitScheduleDragStart = (index: number, event: DragEvent) => {
  visitScheduleDragIndex.value = index;
  visitScheduleDragOverIndex.value = index;
  visitScheduleInsertPosition.value = "before";
  event.dataTransfer?.setData("text/plain", String(index));
  if (event.dataTransfer) {
    event.dataTransfer.effectAllowed = "move";
  }
};

const handleVisitScheduleDragOver = (index: number, event: DragEvent) => {
  if (visitScheduleDragIndex.value === null) return;
  const target = event.currentTarget as HTMLElement | null;
  if (target) {
    const rect = target.getBoundingClientRect();
    visitScheduleInsertPosition.value = event.clientY < rect.top + rect.height / 2 ? "before" : "after";
  }
  visitScheduleDragOverIndex.value = index;
};

const handleVisitScheduleDrop = (targetIndex: number) => {
  const sourceIndex = visitScheduleDragIndex.value;
  if (sourceIndex === null) {
    handleVisitScheduleDragEnd();
    return;
  }
  let insertIndex = visitScheduleInsertPosition.value === "after" ? targetIndex + 1 : targetIndex;
  if (sourceIndex === targetIndex || sourceIndex + 1 === insertIndex) {
    handleVisitScheduleDragEnd();
    return;
  }
  const nextRows = [...form.value.visit_schedule];
  const [movedRow] = nextRows.splice(sourceIndex, 1);
  if (!movedRow) {
    handleVisitScheduleDragEnd();
    return;
  }
  if (sourceIndex < insertIndex) {
    insertIndex -= 1;
  }
  nextRows.splice(insertIndex, 0, movedRow);
  form.value.visit_schedule = nextRows;
  handleVisitScheduleDragEnd();
};

const handleVisitScheduleDragEnd = () => {
  visitScheduleDragIndex.value = null;
  visitScheduleDragOverIndex.value = null;
  visitScheduleInsertPosition.value = "before";
};

const isVisitScheduleInsertTarget = (index: number, position: VisitScheduleInsertPosition) =>
  visitScheduleDragIndex.value !== null &&
  visitScheduleDragIndex.value !== index &&
  visitScheduleDragOverIndex.value === index &&
  visitScheduleInsertPosition.value === position;

const formatVisitSchedule = (rows: VisitScheduleItem[] | undefined | null): string => {
  const normalized = normalizeVisitSchedule(rows);
  if (!normalized.length) return "-";
  return normalized
    .map(
      (row) =>
        `${row.visit_code || "-"}：基线+${row.baseline_offset_days}天，窗口-${row.window_before_days}/+${row.window_after_days}天`
    )
    .join("；");
};

const formatVisitWindow = (row: VisitScheduleItem): string => `-${row.window_before_days} / +${row.window_after_days} 天`;

const emptyProjectInfoDraft = (): ProjectPublishSnapshot => ({
  code: "",
  name: "",
  project_full_name: "",
  sponsor: "",
  protocol_no: "",
  lead_unit: "",
  principal_investigator: "",
  main_pm: "",
  research_analysis: "",
  research_product: "",
  control_product: "",
  indication: "",
  research_population: "",
  research_design: "",
  plan_start_date: "",
  plan_end_date: "",
  planned_site_count: null,
  planned_enrollment_count: null,
  status: "",
  visit_schedule: [],
});

const setupDraft = reactive<SetupConfigDraft>({
  projectInfo: emptyProjectInfoDraft(),
  projectMilestones: [],
  enrollmentPlan: {
    totalTarget: 0,
    startDate: "",
    endDate: "",
    monthlyGoalNote: "",
    stageBreakdown: "",
  },
  siteMilestones: [],
  siteEnrollmentPlans: [],
  centerConfirm: [],
});

const storageKey = computed(() => `ctms_setup_config_draft_${String(route.params.projectId || "")}`);
const projectDraftStorageKey = computed(() => `ctms_setup_project_draft_${String(route.params.projectId || "")}`);
const projectPermissions = ref<Record<string, Record<string, any>> | null>(null);
const hasSetupReadPermission = computed(() => {
  if (isSystemAdmin(authStore.user)) return true;
  if (!projectPermissions.value) return false;
  const rolePerms = Object.values(projectPermissions.value)[0];
  if (!rolePerms) return false;
  return isApiPermissionAllowed(rolePerms["setup_config:update"]);
});
const canManageSetup = computed(() => {
  if (isSystemAdmin(authStore.user)) return true;
  if (!projectPermissions.value) return false;
  const rolePerms = Object.values(projectPermissions.value)[0];
  if (!rolePerms) return false;
  return isApiPermissionAllowed(rolePerms["setup_config:update"]);
});
const isPreviewView = computed(() => setupViewMode.value === "preview");
const isPublishedVersionView = computed(() => setupViewMode.value === "published");
const isPublishedView = computed(() => isPreviewView.value || isPublishedVersionView.value);
const currentSetupDraft = computed<SetupConfigDraft>(() => {
  if (isPublishedVersionView.value && publishedSetupDraft.value) {
    return publishedSetupDraft.value;
  }
  return setupDraft;
});
const canEditSetup = computed(
  () => Boolean(project.value && !project.value?.is_locked && canManageSetup.value && !isPublishedView.value)
);
const canMutateDraft = () => canEditSetup.value && !isPublishedView.value;
const canEditStep1BasicGroup = (group: Exclude<Step1EditSection, "all" | "summary">) =>
  activeStep.value === 0 && isEditing.value && (step1EditSection.value === "all" || step1EditSection.value === group);
const isStep1BasicScopedEdit = computed(
  () => activeStep.value === 0 && isEditing.value && ["project", "research", "execution"].includes(step1EditSection.value)
);
const showStep1BasicTab = computed(
  () => !(activeStep.value === 0 && isEditing.value && step1EditSection.value === "summary")
);
const showStep1SummaryTab = computed(() => !isStep1BasicScopedEdit.value);
const stepActionMode = computed<StepActionMode>(() => {
  if (isPublishedView.value) return "none";
  switch (activeStep.value) {
    case 2:
      return "edit";
    case 0:
      return "none";
    case 1:
      return "project-milestone";
    case 3:
      return "site-milestone";
    case 4:
      return "edit";
    default:
      return "none";
  }
});
const openIndexedEditor = <T>(
  rows: T[],
  rowIndex: number,
  controller: IndexedEditorController,
  fillForm: (row: T) => void
) => {
  if (!canMutateDraft()) return false;
  const row = rows[rowIndex];
  if (!row) return false;
  controller.index.value = rowIndex;
  fillForm(row);
  controller.visible.value = true;
  return true;
};
const getEditingIndex = (controller: IndexedEditorController, length: number) => {
  const idx = controller.index.value;
  if (idx < 0 || idx >= length) return -1;
  return idx;
};
const closeIndexedEditor = (controller: IndexedEditorController, successMessage: string) => {
  controller.visible.value = false;
  controller.index.value = -1;
  ElMessage.success(successMessage);
};
const canSaveDraftAction = computed(() => Boolean(project.value && canManageSetup.value && !project.value?.is_locked && !isPublishedView.value));
const canPublishAction = computed(() => Boolean(project.value && canManageSetup.value && !project.value?.is_locked));
const currentStepTitle = computed(() => `第${activeStep.value + 1}步：${steps[activeStep.value]?.label || ""}`);
const setupPublishedVersionText = computed(() => {
  return setupPublishedVersion.value || "v0";
});
const hasProjectPendingChanges = computed(() => hasFormUnsavedChanges.value);
const draftSyncStatus = computed<DraftSyncStatus>(() => {
  const formDirty = Boolean(formBaselineSnapshot.value && serializeFormForCompare() !== formBaselineSnapshot.value);
  if (formDirty || setupDirtySinceLastPersist.value || Boolean(autoSaveTimer.value)) {
    return "DIRTY_UNSAVED";
  }
  return setupSaveMeta.value.serverSynced ? "SYNCED" : "LOCAL_ONLY";
});
const setupWorkflowStatus = computed<SetupWorkflowStatus>(() =>
  resolveSetupWorkflowStatus(draftSyncStatus.value, {
    canStrictlyDetectNoDiff: canStrictlyDetectNoDiff.value,
    hasPublishableDiff: hasPublishableDiff.value,
  })
);
const setupWorkflowTagMeta = computed<SetupWorkflowTagMeta>(() => SETUP_WORKFLOW_TAG_META[setupWorkflowStatus.value]);
const setupWorkflowTagLabel = computed(() => setupWorkflowTagMeta.value.label);
const setupWorkflowTagType = computed<"success" | "warning" | "info">(() => setupWorkflowTagMeta.value.type);
const isSetupDraftEffectivelyEmpty = (draft: SetupConfigDraft): boolean => {
  if (!isProjectPublishSnapshotEmpty(draft.projectInfo)) return false;
  if (draft.projectMilestones.length > 0) return false;
  if (draft.siteMilestones.length > 0) return false;
  if (draft.siteEnrollmentPlans.length > 0) return false;
  if (draft.centerConfirm.length > 0) return false;
  const plan = draft.enrollmentPlan;
  const normalizedTotal = Number(plan.totalTarget || 0);
  if (!Number.isFinite(normalizedTotal) || normalizedTotal !== 0) return false;
  if ((plan.startDate || "").trim()) return false;
  if ((plan.endDate || "").trim()) return false;
  if ((plan.monthlyGoalNote || "").trim()) return false;
  if ((plan.stageBreakdown || "").trim()) return false;
  return true;
};
const isAtomicSetupDraft = (draft: SetupConfigDraft): boolean =>
  !isProjectPublishSnapshotEmpty(draft.projectInfo) || isSetupDraftEffectivelyEmpty(draft);
const shouldShowEmptyDraftBaseInfo = computed(() => isSetupDraftEffectivelyEmpty(setupDraft));
const setupDraftBaseVersionText = computed(() => {
  const explicit = (setupActiveDraftBaseVersion.value || "").trim();
  if (explicit) return explicit;
  const draftBranch = (setupCurrentBranchName.value || "main").trim() || "main";
  const publishedBranch = (setupPublishedBranchName.value || "main").trim() || "main";
  if (draftBranch === publishedBranch && setupPublishedVersionText.value && setupPublishedVersionText.value !== "v0") {
    return setupPublishedVersionText.value;
  }
  return "-";
});
const setupDraftBaseVersionDisplay = computed(() => (shouldShowEmptyDraftBaseInfo.value ? "-" : setupDraftBaseVersionText.value));
const setupDraftBranchDisplay = computed(() =>
  shouldShowEmptyDraftBaseInfo.value ? "-" : ((setupCurrentBranchName.value || "main").trim() || "main")
);
const setupDraftStatusDisplay = computed(() => (shouldShowEmptyDraftBaseInfo.value ? "-" : setupWorkflowTagLabel.value));
const serializeFormForCompare = (): string =>
  JSON.stringify({
    code: form.value.code || "",
    name: form.value.name || "",
    project_full_name: form.value.project_full_name || "",
    sponsor: form.value.sponsor || "",
    protocol_no: form.value.protocol_no || "",
    lead_unit: form.value.lead_unit || "",
    principal_investigator: form.value.principal_investigator || "",
    main_pm: form.value.main_pm || "",
    research_analysis: form.value.research_analysis || "",
    research_product: form.value.research_product || "",
    control_product: form.value.control_product || "",
    indication: form.value.indication || "",
    research_population: form.value.research_population || "",
    research_design: form.value.research_design || "",
    planned_site_count: form.value.planned_site_count,
    // These three fields are setup-linked and persisted by setup draft/publish flow.
    // Excluding them avoids false "project unsaved" prompts after saving setup config.
    plan_start_date: "",
    plan_end_date: "",
    planned_enrollment_count: null,
    phase: form.value.phase || "",
    status: form.value.status || "",
    visit_schedule: normalizeVisitSchedule(form.value.visit_schedule),
  });
const setupDraftForPersistence = (): SetupConfigDraft => ({
  ...clone(setupDraft),
  projectInfo: buildProjectPublishSnapshot(),
});
const syncProjectInfoIntoSetupDraft = () => {
  setupDraft.projectInfo = buildProjectPublishSnapshot();
};
const serializeSetupForCompare = (): string => JSON.stringify(setupDraftForPersistence());
const buildProjectPublishSnapshotFromStudy = (study: Partial<Study> | null | undefined): ProjectPublishSnapshot => ({
  code: study?.code || "",
  name: study?.name || "",
  project_full_name: study?.project_full_name || "",
  sponsor: study?.sponsor || "",
  protocol_no: study?.protocol_no || "",
  lead_unit: study?.lead_unit || "",
  principal_investigator: study?.principal_investigator || "",
  main_pm: study?.main_pm || "",
  research_analysis: study?.research_analysis || "",
  research_product: study?.research_product || "",
  control_product: study?.control_product || "",
  indication: study?.indication || "",
  research_population: study?.research_population || "",
  research_design: study?.research_design || "",
  plan_start_date: study?.plan_start_date || "",
  plan_end_date: study?.plan_end_date || "",
  planned_site_count: study?.planned_site_count ?? null,
  planned_enrollment_count: study?.planned_enrollment_count ?? null,
  status: study?.status || "",
  visit_schedule: normalizeVisitSchedule(study?.visit_schedule),
});
const buildProjectPublishSnapshot = (): ProjectPublishSnapshot =>
  buildProjectPublishSnapshotFromStudy({
    code: form.value.code,
    name: form.value.name,
    project_full_name: form.value.project_full_name,
    sponsor: form.value.sponsor,
    protocol_no: form.value.protocol_no,
    lead_unit: form.value.lead_unit,
    principal_investigator: form.value.principal_investigator,
    main_pm: form.value.main_pm,
    research_analysis: form.value.research_analysis,
    research_product: form.value.research_product,
    control_product: form.value.control_product,
    indication: form.value.indication,
    research_population: form.value.research_population,
    research_design: form.value.research_design,
    plan_start_date: form.value.plan_start_date,
    plan_end_date: form.value.plan_end_date,
    planned_site_count: form.value.planned_site_count,
    planned_enrollment_count: form.value.planned_enrollment_count,
    status: form.value.status,
    visit_schedule: normalizeVisitSchedule(form.value.visit_schedule),
  });
const serializeProjectForPublishCompare = (): string => JSON.stringify(buildProjectPublishSnapshot());
const hasFormUnsavedChanges = computed(() => Boolean(formBaselineSnapshot.value && serializeFormForCompare() !== formBaselineSnapshot.value));
const hasSetupDiffFromServer = (): boolean =>
  Boolean(setupServerSnapshot.value && serializeSetupForCompare() !== setupServerSnapshot.value);
const clearPendingAutoSave = () => {
  if (!autoSaveTimer.value) return;
  window.clearTimeout(autoSaveTimer.value);
  autoSaveTimer.value = undefined;
};
const refreshSetupDirtyState = (): boolean => {
  setupDirtySinceLastPersist.value = hasSetupDiffFromServer();
  return setupDirtySinceLastPersist.value;
};
const markServerSyncedIfNoLocalChanges = () => {
  if (setupDirtySinceLastPersist.value || autoSaveTimer.value) return;
  if (setupSaveMeta.value.serverSynced) return;
  setupSaveMeta.value = {
    ...setupSaveMeta.value,
    serverSynced: true,
  };
};
const reconcileDraftSyncStateBeforePublish = () => {
  const changed = refreshSetupDirtyState();
  if (changed) return;
  clearPendingAutoSave();
  markSetupSynced();
  if (setupSaveMeta.value.serverSynced) {
    clearLocalSetupDraft();
    clearLocalProjectDraft();
  }
};
const hasUnsavedChanges = computed(
  () => hasFormUnsavedChanges.value || setupDirtySinceLastPersist.value || Boolean(autoSaveTimer.value)
);
const hasSetupDraftUnsavedChanges = computed(() => setupDirtySinceLastPersist.value || Boolean(autoSaveTimer.value));
const hasLeaveGuardChanges = computed(
  () => hasFormUnsavedChanges.value || hasSetupDraftUnsavedChanges.value || draftSyncStatus.value !== "SYNCED"
);
const setupSectionStepMap: Record<string, number> = {
  projectMilestones: 1,
  enrollmentPlan: 2,
  siteMilestones: 3,
  siteEnrollmentPlans: 4,
  centerConfirm: 5,
};
const currentStepErrors = computed(() => {
  const grouped = groupErrorsBySection(
    Object.entries(serverFieldErrors.value).map(([field, message]) => ({ field, message }))
  );
  const section = Object.keys(setupSectionStepMap).find((key) => setupSectionStepMap[key] === activeStep.value);
  if (!section) return [];
  return grouped[section] || [];
});

type DisplayInfoItem = {
  label: string;
  value: string | number | null | undefined;
  updated?: boolean;
};

const shouldShowPreviewUpdateMark = computed(() => isPreviewView.value && !isFirstPublish.value);
const currentProjectPublishSnapshot = computed<ProjectPublishSnapshot>(() => {
  if (isPublishedVersionView.value && publishedProjectSnapshot.value) {
    return publishedProjectSnapshot.value;
  }
  return 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(currentEnrollmentPlanCycle.value, publishedEnrollmentPlanParsed.value.cycle);
  }
  return isPreviewValueUpdated(currentSetupDraft.value.enrollmentPlan[field], publishCompareBase.value.enrollmentPlan[field]);
};
const isProjectEnrollmentPeriodUpdated = (key: string | null): boolean => {
  if (!key) return false;
  const cycle = currentEnrollmentPlanCycle.value;
  const localValue = currentEnrollmentTargetsByCycle.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 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 = currentEnrollmentPlanCycle.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: currentProjectPublishSnapshot.value.name || "-", updated: isProjectSnapshotFieldUpdated("name") },
  { label: "项目全称", value: currentProjectPublishSnapshot.value.project_full_name || "-", updated: isProjectSnapshotFieldUpdated("project_full_name") },
  { label: "申办方", value: currentProjectPublishSnapshot.value.sponsor || "-", updated: isProjectSnapshotFieldUpdated("sponsor") },
  { label: "项目编号", value: currentProjectPublishSnapshot.value.code || "-", updated: isProjectSnapshotFieldUpdated("code") },
  { label: "方案号", value: currentProjectPublishSnapshot.value.protocol_no || "-", updated: isProjectSnapshotFieldUpdated("protocol_no") },
  { label: "组长单位", value: currentProjectPublishSnapshot.value.lead_unit || "-", updated: isProjectSnapshotFieldUpdated("lead_unit") },
  { label: "主要研究者", value: currentProjectPublishSnapshot.value.principal_investigator || "-", updated: isProjectSnapshotFieldUpdated("principal_investigator") },
  { label: "主PM", value: currentProjectPublishSnapshot.value.main_pm || "-", updated: isProjectSnapshotFieldUpdated("main_pm") },
]);

const researchInfoItems = computed<DisplayInfoItem[]>(() => [
  { label: "研究分期", value: currentProjectPublishSnapshot.value.research_analysis || "-", updated: isProjectSnapshotFieldUpdated("research_analysis") },
  { label: "研究产品", value: currentProjectPublishSnapshot.value.research_product || "-", updated: isProjectSnapshotFieldUpdated("research_product") },
  { label: "对照产品", value: currentProjectPublishSnapshot.value.control_product || "-", updated: isProjectSnapshotFieldUpdated("control_product") },
  { label: "适应症", value: currentProjectPublishSnapshot.value.indication || "-", updated: isProjectSnapshotFieldUpdated("indication") },
  { label: "研究人群", value: currentProjectPublishSnapshot.value.research_population || "-", updated: isProjectSnapshotFieldUpdated("research_population") },
  { label: "研究设计", value: currentProjectPublishSnapshot.value.research_design || "-", updated: isProjectSnapshotFieldUpdated("research_design") },
]);

const executionInfoItems = computed<DisplayInfoItem[]>(() => [
  { label: "计划开始日期", value: currentProjectPublishSnapshot.value.plan_start_date || "-", updated: isProjectSnapshotFieldUpdated("plan_start_date") },
  { label: "计划结束日期", value: currentProjectPublishSnapshot.value.plan_end_date || "-", updated: isProjectSnapshotFieldUpdated("plan_end_date") },
  { label: "项目状态", value: statusLabel(currentProjectPublishSnapshot.value.status || "DRAFT"), updated: isProjectSnapshotFieldUpdated("status") },
  { label: "计划中心数", value: toDisplayNumber(currentProjectPublishSnapshot.value.planned_site_count), updated: isProjectSnapshotFieldUpdated("planned_site_count") },
  {
    label: "计划入组数",
    value: toDisplayNumber(currentSetupDraft.value.enrollmentPlan.totalTarget),
    updated: isEnrollmentPlanFieldUpdated("totalTarget"),
  },
]);

const summaryVisitScheduleRows = computed<VisitScheduleItem[]>(() => normalizeVisitSchedule(currentProjectPublishSnapshot.value.visit_schedule));

const rules = ref<FormRules>({
  code: [{ required: true, message: requiredMessage(TEXT.common.fields.projectCode), trigger: "blur" }],
  name: [{ required: true, message: requiredMessage(TEXT.common.fields.projectName), trigger: "blur" }],
  status: [{ required: true, message: requiredMessage(TEXT.common.fields.status), trigger: "change" }],
});

const makeId = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const nowString = (): string => new Date().toLocaleString("zh-CN", { hour12: false });
const nowMs = (): number => Date.now();
const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
const formatDisplayTime = (value?: string | null): string => {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return date.toLocaleString("zh-CN", { hour12: false });
};
const clearSetupValidationErrors = () => {
  serverFieldErrors.value = {};
};
const markSetupSynced = () => {
  setupDirtySinceLastPersist.value = false;
};
const markSetupServerSynced = () => {
  clearPendingAutoSave();
  setupServerSnapshot.value = serializeSetupForCompare();
  markSetupSynced();
  clearLocalSetupDraft();
  clearLocalProjectDraft();
};
const updateLastServerSaveMeta = (updatedAt?: string | null, savedBy?: string | null) => {
  lastServerSaveMeta.value = {
    updatedAt: formatDisplayTime(updatedAt) || lastServerSaveMeta.value.updatedAt || "-",
    savedBy: savedBy || "-",
  };
};
const parseLocalDraftEnvelope = <T>(
  raw: string,
  parseData: (candidate: unknown) => T | null,
  serverVersion?: number
): LocalDraftLoadResult<T> | null => {
  const parsed = JSON.parse(raw) as LocalDraftEnvelope<unknown>;
  const parsedVersion = parsed?.version;
  if (parsedVersion !== 1 && parsedVersion !== DRAFT_VERSION) return null;
  const parsedData = parseData(parsed?.data);
  if (!parsedData) return null;
  const savedAtMs = typeof parsed?.savedAtMs === "number" ? parsed.savedAtMs : 0;
  const ttlMs = typeof parsed?.ttlMs === "number" && parsed.ttlMs > 0 ? parsed.ttlMs : LOCAL_DRAFT_TTL_MS;
  const serverVersionAtCache =
    typeof parsed?.serverVersionAtCache === "number" ? parsed.serverVersionAtCache : null;
  const expired = Boolean(savedAtMs && savedAtMs + ttlMs < nowMs());
  const conflict = Boolean(
    typeof serverVersion === "number" &&
      serverVersionAtCache !== null &&
      serverVersionAtCache !== serverVersion
  );
  return {
    data: parsedData,
    savedAt: parsed?.savedAt || nowString(),
    expired,
    conflict,
    serverVersionAtCache,
  };
};

const normalizeEnrollmentTarget = (value: number | null | undefined): number => {
  if (value === null || value === undefined) return 0;
  if (!Number.isFinite(value)) return 0;
  if (value < 0) return 0;
  return Math.floor(value);
};

const normalizePlanDate = (value: string | null | undefined): string => {
  if (!value) return "";
  return String(value);
};
const parseDateOnlyValue = (value: string | null | undefined): Date | null => {
  const normalized = normalizePlanDate(value);
  if (!normalized) return null;
  const d = new Date(`${normalized}T00:00:00`);
  if (Number.isNaN(d.getTime())) return null;
  return d;
};
const calculateDurationDays = (startDate: string | null | undefined, endDate: string | null | undefined): number => {
  const start = parseDateOnlyValue(startDate);
  const end = parseDateOnlyValue(endDate);
  if (!start || !end) return 0;
  if (end < start) return 0;
  const diffMs = end.getTime() - start.getTime();
  return Math.floor(diffMs / 86400000) + 1;
};
const resolveProjectMilestoneStart = (row: Partial<ProjectMilestoneDraft> | null | undefined): string =>
  normalizePlanDate(row?.startDate || row?.planDate || "");
const resolveProjectMilestoneEnd = (row: Partial<ProjectMilestoneDraft> | null | undefined): string =>
  normalizePlanDate(row?.endDate || row?.planDate || "");
const resolveProjectMilestoneDurationDays = (row: Partial<ProjectMilestoneDraft> | null | undefined): number => {
  const rawDays = Number((row as any)?.durationDays);
  if (Number.isFinite(rawDays) && rawDays > 0) return Math.floor(rawDays);
  const startDate = resolveProjectMilestoneStart(row);
  const endDate = resolveProjectMilestoneEnd(row);
  if (!startDate && !endDate) return 0;
  return calculateDurationDays(startDate, endDate) || 1;
};
const formatProjectMilestoneDuration = (row: Partial<ProjectMilestoneDraft> | null | undefined): string => {
  const days = resolveProjectMilestoneDurationDays(row);
  return days > 0 ? `${days}天` : "-";
};
const normalizeProjectMilestoneRows = (rows: ProjectMilestoneDraft[]): ProjectMilestoneDraft[] =>
  rows.map((row) => {
    const startDate = resolveProjectMilestoneStart(row);
    const endDate = resolveProjectMilestoneEnd(row);
    const durationDays = resolveProjectMilestoneDurationDays({ ...row, startDate, endDate });
    return {
      ...row,
      planDate: startDate,
      startDate,
      endDate,
      durationDays: durationDays > 0 ? durationDays : 1,
    };
  });
const getProjectPlanWindow = (): { start: string; end: string } => ({
  start: normalizePlanDate(form.value.plan_start_date || project.value?.plan_start_date || ""),
  end: normalizePlanDate(form.value.plan_end_date || project.value?.plan_end_date || ""),
});

const ENROLLMENT_STAGE_PAYLOAD_TYPE = "enrollment_plan_v2";
const SITE_ENROLLMENT_STAGE_PAYLOAD_TYPE = "site_enrollment_plan_v2";
const enrollmentCycleLabelMap: Record<EnrollmentCycle, string> = {
  month: "月",
  quarter: "季度",
};
const enrollmentKeyPatternMap: Record<EnrollmentCycle, RegExp> = {
  month: /^\d{4}-(0[1-9]|1[0-2])$/,
  quarter: /^\d{4}-Q[1-4]$/,
};
const normalizeEnrollmentCycle = (value: unknown): EnrollmentCycle => {
  const normalized = String(value || "").trim().toLowerCase();
  if (normalized === "quarter") return "quarter";
  return "month";
};
const createEmptyEnrollmentTargetsByCycle = (): Record<EnrollmentCycle, Record<string, number>> => ({
  month: {},
  quarter: {},
});
const normalizeCycleTargets = (cycle: EnrollmentCycle, raw: unknown): Record<string, number> => {
  if (!raw || typeof raw !== "object") return {};
  const pattern = enrollmentKeyPatternMap[cycle];
  const normalized: Record<string, number> = {};
  Object.entries(raw as Record<string, unknown>).forEach(([key, value]) => {
    if (!pattern.test(key)) return;
    const numeric = Number(value);
    if (!Number.isFinite(numeric)) return;
    normalized[key] = normalizeEnrollmentTarget(numeric);
  });
  return normalized;
};
const parseEnrollmentStageBreakdown = (
  raw: string | null | undefined
): { cycle: EnrollmentCycle; targetsByCycle: Record<EnrollmentCycle, Record<string, number>> } => {
  const empty = {
    cycle: "month" as EnrollmentCycle,
    targetsByCycle: createEmptyEnrollmentTargetsByCycle(),
  };
  if (!raw || !String(raw).trim()) return empty;
  try {
    const parsed = JSON.parse(String(raw));
    if (!parsed || typeof parsed !== "object") return empty;
    const data = parsed as Record<string, unknown>;
    const cycle = normalizeEnrollmentCycle(data.cycle);
    const byCycle = createEmptyEnrollmentTargetsByCycle();
    const valuesByCycleRaw = data.valuesByCycle;
    if (valuesByCycleRaw && typeof valuesByCycleRaw === "object") {
      byCycle.month = normalizeCycleTargets("month", (valuesByCycleRaw as Record<string, unknown>).month);
      byCycle.quarter = normalizeCycleTargets("quarter", (valuesByCycleRaw as Record<string, unknown>).quarter);
      return { cycle, targetsByCycle: byCycle };
    }
    const legacyTargetsRaw = data.monthlyTargets || data.values;
    const fallbackCycle = data.monthlyTargets ? "month" : cycle;
    byCycle[fallbackCycle] = normalizeCycleTargets(fallbackCycle, legacyTargetsRaw);
    return { cycle, targetsByCycle: byCycle };
  } catch {
    return empty;
  }
};
const buildEnrollmentStageBreakdown = (
  payload: { cycle: EnrollmentCycle; targetsByCycle: Record<EnrollmentCycle, Record<string, number>> }
): string => {
  const byCycle: Record<EnrollmentCycle, Record<string, number>> = {
    month: normalizeCycleTargets("month", payload.targetsByCycle?.month),
    quarter: normalizeCycleTargets("quarter", payload.targetsByCycle?.quarter),
  };
  return JSON.stringify({
    type: ENROLLMENT_STAGE_PAYLOAD_TYPE,
    cycle: normalizeEnrollmentCycle(payload.cycle),
    valuesByCycle: byCycle,
    values: byCycle[normalizeEnrollmentCycle(payload.cycle)] || {},
  });
};
const parseSiteEnrollmentStageBreakdown = (raw: string | null | undefined): Record<EnrollmentCycle, Record<string, number>> => {
  if (!raw || !String(raw).trim()) return createEmptyEnrollmentTargetsByCycle();
  try {
    const parsed = JSON.parse(String(raw));
    if (!parsed || typeof parsed !== "object") return createEmptyEnrollmentTargetsByCycle();
    const data = parsed as Record<string, unknown>;
    const byCycle = createEmptyEnrollmentTargetsByCycle();
    const valuesByCycleRaw = data.valuesByCycle;
    if (valuesByCycleRaw && typeof valuesByCycleRaw === "object") {
      byCycle.month = normalizeCycleTargets("month", (valuesByCycleRaw as Record<string, unknown>).month);
      byCycle.quarter = normalizeCycleTargets("quarter", (valuesByCycleRaw as Record<string, unknown>).quarter);
      return byCycle;
    }
    byCycle.month = normalizeCycleTargets("month", data.values || data.monthlyTargets);
    byCycle.quarter = normalizeCycleTargets("quarter", data.quarterlyTargets);
    return byCycle;
  } catch {
    return createEmptyEnrollmentTargetsByCycle();
  }
};
const buildSiteEnrollmentStageBreakdown = (payload: Record<EnrollmentCycle, Record<string, number>>): string => {
  const byCycle: Record<EnrollmentCycle, Record<string, number>> = {
    month: normalizeCycleTargets("month", payload.month),
    quarter: normalizeCycleTargets("quarter", payload.quarter),
  };
  return JSON.stringify({
    type: SITE_ENROLLMENT_STAGE_PAYLOAD_TYPE,
    valuesByCycle: byCycle,
  });
};
const parseDateParts = (value: string | null | undefined): { year: number; month: number; day: number } | null => {
  const text = String(value || "").trim();
  const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
  if (!matched) return null;
  const year = Number(matched[1]);
  const month = Number(matched[2]);
  const day = Number(matched[3]);
  if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
  if (month < 1 || month > 12 || day < 1 || day > 31) return null;
  return { year, month, day };
};
const toDateOnly = (value: string | null | undefined): Date | null => {
  const parts = parseDateParts(value);
  if (!parts) return null;
  const date = new Date(parts.year, parts.month - 1, parts.day);
  if (date.getFullYear() !== parts.year || date.getMonth() !== parts.month - 1 || date.getDate() !== parts.day) {
    return null;
  }
  return date;
};
const pad2 = (value: number): string => String(value).padStart(2, "0");
const getQuarter = (month: number): number => Math.floor((month - 1) / 3) + 1;
const toYearMonthIndex = (year: number, month: number): number => year * 12 + (month - 1);
const buildYearMonthKey = (year: number, month: number): string => `${year}-${String(month).padStart(2, "0")}`;
const buildEnrollmentSlotsByCycle = (
  cycle: EnrollmentCycle,
  startDateText: string | null | undefined,
  endDateText: string | null | undefined
): EnrollmentPeriodSlot[] => {
  const start = parseDateParts(startDateText);
  const end = parseDateParts(endDateText);
  if (!start || !end) return [];
  if (toYearMonthIndex(start.year, start.month) > toYearMonthIndex(end.year, end.month)) return [];
  if (cycle === "month") {
    const result: EnrollmentPeriodSlot[] = [];
    let year = start.year;
    let month = start.month;
    while (toYearMonthIndex(year, month) <= toYearMonthIndex(end.year, end.month)) {
      result.push({
        key: buildYearMonthKey(year, month),
        label: `${month}月`,
        groupLabel: `${year}`,
      });
      month += 1;
      if (month > 12) {
        month = 1;
        year += 1;
      }
    }
    return result;
  }
  const startDate = toDateOnly(startDateText);
  const endDate = toDateOnly(endDateText);
  if (!startDate || !endDate || startDate.getTime() > endDate.getTime()) return [];
  const result: EnrollmentPeriodSlot[] = [];
  let year = start.year;
  let quarter = getQuarter(start.month);
  const endQuarter = getQuarter(end.month);
  while (year < end.year || (year === end.year && quarter <= endQuarter)) {
    result.push({
      key: `${year}-Q${quarter}`,
      label: `Q${quarter}`,
      groupLabel: `${year}`,
    });
    quarter += 1;
    if (quarter > 4) {
      quarter = 1;
      year += 1;
    }
  }
  return result;
};
const currentEnrollmentPlanParsed = computed(() =>
  parseEnrollmentStageBreakdown(currentSetupDraft.value.enrollmentPlan.stageBreakdown)
);
const currentEnrollmentPlanCycle = computed<EnrollmentCycle>(() => {
  if (isPublishedVersionView.value) return currentEnrollmentPlanParsed.value.cycle;
  return enrollmentPlanCycle.value;
});
const currentEnrollmentTargetsByCycle = computed<Record<EnrollmentCycle, Record<string, number>>>(() => {
  if (isPublishedVersionView.value) return currentEnrollmentPlanParsed.value.targetsByCycle;
  return enrollmentTargetsByCycle.value;
});
const currentEnrollmentSlots = computed(() =>
  buildEnrollmentSlotsByCycle(
    currentEnrollmentPlanCycle.value,
    currentSetupDraft.value.enrollmentPlan.startDate,
    currentSetupDraft.value.enrollmentPlan.endDate
  )
);
const enrollmentPlanMonthSlots = computed(() => {
  const slots = buildEnrollmentSlotsByCycle(
    "month",
    currentSetupDraft.value.enrollmentPlan.startDate,
    currentSetupDraft.value.enrollmentPlan.endDate
  );
  return slots.map((slot) => ({
    year: Number(slot.key.slice(0, 4)),
    month: Number(slot.key.slice(5, 7)),
    key: slot.key,
  }));
});
const enrollmentPlanYearRows = computed<EnrollmentYearRow[]>(() => {
  const slots = enrollmentPlanMonthSlots.value;
  if (!slots.length) return [];
  const keySet = new Set(slots.map((slot) => slot.key));
  const firstYear = slots[0].year;
  const lastYear = slots[slots.length - 1].year;
  const rows: EnrollmentYearRow[] = [];
  for (let year = firstYear; year <= lastYear; year += 1) {
    rows.push({
      year,
      cells: Array.from({ length: 12 }, (_, idx) => {
        const key = buildYearMonthKey(year, idx + 1);
        return { key: keySet.has(key) ? key : null };
      }),
    });
  }
  return rows;
});
const enrollmentPlanCycleLabel = computed(() => enrollmentCycleLabelMap[currentEnrollmentPlanCycle.value] || "月");
const buildEnrollmentCycleGridColumns = (): EnrollmentCycleGridColumn[] => {
  return Array.from({ length: 4 }, (_, idx) => {
    const quarter = idx + 1;
    const key = `Q${quarter}`;
    return { key, label: key };
  });
};
const resolveEnrollmentCycleColumnKey = (periodKey: string): string | null => {
  const matched = /^(\d{4})-Q([1-4])$/.exec(periodKey);
  return matched ? `Q${matched[2]}` : null;
};
const enrollmentCycleGridColumns = computed<EnrollmentCycleGridColumn[]>(() => {
  if (currentEnrollmentPlanCycle.value === "month") return [];
  return buildEnrollmentCycleGridColumns();
});
const enrollmentCycleRowLabel = computed(() => "年份");
const enrollmentCycleGridRows = computed<EnrollmentCycleGridRow[]>(() => {
  if (currentEnrollmentPlanCycle.value === "month") return [];
  const columns = enrollmentCycleGridColumns.value;
  if (!columns.length) return [];
  const columnIndexMap = new Map<string, number>();
  columns.forEach((column, idx) => columnIndexMap.set(column.key, idx));
  const rowMap = new Map<string, EnrollmentCycleGridRow>();
  currentEnrollmentSlots.value.forEach((slot) => {
    const columnKey = resolveEnrollmentCycleColumnKey(slot.key);
    if (!columnKey) return;
    const columnIndex = columnIndexMap.get(columnKey);
    if (columnIndex === undefined) return;
    let row = rowMap.get(slot.groupLabel);
    if (!row) {
      row = {
        groupLabel: slot.groupLabel,
        cells: columns.map(() => ({ key: null })),
      };
      rowMap.set(slot.groupLabel, row);
    }
    row.cells[columnIndex] = { key: slot.key };
  });
  return Array.from(rowMap.values());
});
const getEnrollmentPeriodTarget = (key: string | null): number => {
  if (!key) return 0;
  const value = currentEnrollmentTargetsByCycle.value[currentEnrollmentPlanCycle.value]?.[key];
  if (value === null || value === undefined) return 0;
  return normalizeEnrollmentTarget(value);
};
const displayEnrollmentPeriodTarget = (key: string | null): string => {
  if (!key) return "-";
  const value = currentEnrollmentTargetsByCycle.value[currentEnrollmentPlanCycle.value]?.[key];
  if (value === null || value === undefined) return "-";
  return String(normalizeEnrollmentTarget(value));
};
const setEnrollmentPeriodTarget = (key: string | null, value: number | null) => {
  if (!key) return;
  const cycle = enrollmentPlanCycle.value;
  const nextTargets = { ...(enrollmentTargetsByCycle.value[cycle] || {}) };
  if (value === null || value === undefined) {
    delete nextTargets[key];
  } else {
    nextTargets[key] = normalizeEnrollmentTarget(value);
  }
  enrollmentTargetsByCycle.value = {
    ...enrollmentTargetsByCycle.value,
    [cycle]: nextTargets,
  };
};
const clearEnrollmentPeriodPlan = () => {
  if (!isEditing.value || isPublishedView.value || !canEditSetup.value) return;
  const cycle = enrollmentPlanCycle.value;
  const scopedKeys = new Set(currentEnrollmentSlots.value.map((item) => item.key));
  if (!scopedKeys.size) {
    ElMessage.warning("请先填写有效的计划入组日期范围");
    return;
  }
  const nextTargets = { ...(enrollmentTargetsByCycle.value[cycle] || {}) };
  scopedKeys.forEach((key) => {
    delete nextTargets[key];
  });
  enrollmentTargetsByCycle.value = {
    ...enrollmentTargetsByCycle.value,
    [cycle]: nextTargets,
  };
  ElMessage.success(`已清空当前日期范围内的${enrollmentPlanCycleLabel.value}周期计划`);
};
const quickFillEnrollmentPeriodPlan = () => {
  if (!isEditing.value || isPublishedView.value || !canEditSetup.value) return;
  const cycle = enrollmentPlanCycle.value;
  const slots = currentEnrollmentSlots.value;
  if (!slots.length) {
    ElMessage.warning("请先填写有效的计划入组日期范围");
    return;
  }
  const totalTarget = normalizeEnrollmentTarget(setupDraft.enrollmentPlan.totalTarget);
  const base = Math.floor(totalTarget / slots.length);
  let remainder = totalTarget - base * slots.length;
  const nextTargets = { ...(enrollmentTargetsByCycle.value[cycle] || {}) };
  slots.forEach((slot) => {
    const extra = remainder > 0 ? 1 : 0;
    nextTargets[slot.key] = base + extra;
    if (remainder > 0) remainder -= 1;
  });
  enrollmentTargetsByCycle.value = {
    ...enrollmentTargetsByCycle.value,
    [cycle]: nextTargets,
  };
  ElMessage.success(`已按${enrollmentPlanCycleLabel.value}周期快速均分入组计划`);
};
const projectMonthlyPlannedCount = computed(() => {
  return currentEnrollmentSlots.value.reduce((sum, slot) => sum + getEnrollmentPeriodTarget(slot.key), 0);
});
const projectMonthlyPendingCount = computed(() => {
  const totalTarget = normalizeEnrollmentTarget(currentSetupDraft.value.enrollmentPlan.totalTarget);
  return Math.max(totalTarget - projectMonthlyPlannedCount.value, 0);
});
const projectMonthlyOverflowCount = computed(() => {
  const totalTarget = normalizeEnrollmentTarget(currentSetupDraft.value.enrollmentPlan.totalTarget);
  return Math.max(projectMonthlyPlannedCount.value - totalTarget, 0);
});
const centerEnrollmentTargetMap = computed(() => {
  const map = new Map<string, number>();
  currentSetupDraft.value.siteEnrollmentPlans.forEach((row) => {
    const siteId = String(row.siteId || "").trim();
    if (!siteId) return;
    map.set(siteId, normalizeEnrollmentTarget(row.target));
  });
  return map;
});
const centerActiveSiteCount = computed(() => siteOptions.value.filter((site) => site.is_active !== false).length);
const centerConfiguredSiteCount = computed(() => {
  const plannedSiteIds = centerEnrollmentTargetMap.value;
  return siteOptions.value.filter((site) => site.is_active !== false && plannedSiteIds.has(String(site.id || "").trim())).length;
});
const centerPlannedCount = computed(() => {
  return Array.from(centerEnrollmentTargetMap.value.values()).reduce((sum, target) => sum + target, 0);
});
const centerPendingCount = computed(() => {
  const totalTarget = normalizeEnrollmentTarget(currentSetupDraft.value.enrollmentPlan.totalTarget);
  return Math.max(totalTarget - centerPlannedCount.value, 0);
});
const centerOverflowCount = computed(() => {
  const totalTarget = normalizeEnrollmentTarget(currentSetupDraft.value.enrollmentPlan.totalTarget);
  return Math.max(centerPlannedCount.value - totalTarget, 0);
});
const centerUnplannedSiteCount = computed(() => {
  const activeSites = siteOptions.value.filter((site) => site.is_active !== false);
  if (!activeSites.length) return 0;
  const plannedSiteIds = centerEnrollmentTargetMap.value;
  return activeSites.filter((site) => !plannedSiteIds.has(site.id)).length;
});
const getSiteEnrollmentTargetsByCycle = (row: SiteEnrollmentPlanDraft): Record<EnrollmentCycle, Record<string, number>> => {
  const parsed = parseSiteEnrollmentStageBreakdown(row.stageBreakdown);
  return {
    month: { ...parsed.month },
    quarter: { ...parsed.quarter },
  };
};
const sumSiteEnrollmentTargetForCurrentCycle = (targets: Record<EnrollmentCycle, Record<string, number>>): number => {
  const slotKeys = new Set(currentEnrollmentSlots.value.map((item) => item.key));
  const activeTargets = targets[currentEnrollmentPlanCycle.value] || {};
  let sum = 0;
  Object.entries(activeTargets).forEach(([key, value]) => {
    if (!slotKeys.has(key)) return;
    sum += normalizeEnrollmentTarget(value);
  });
  return sum;
};
const centerCyclePlannedCount = computed(() => {
  return currentSetupDraft.value.siteEnrollmentPlans.reduce((sum, row) => {
    return sum + sumSiteEnrollmentTargetForCurrentCycle(getSiteEnrollmentTargetsByCycle(row));
  }, 0);
});
const centerCyclePendingCount = computed(() => Math.max(centerPlannedCount.value - centerCyclePlannedCount.value, 0));
const centerCycleOverflowCount = computed(() => Math.max(centerCyclePlannedCount.value - centerPlannedCount.value, 0));
const getSiteEnrollmentPeriodTarget = (row: SiteEnrollmentPlanDraft, key: string | null): number => {
  if (!key) return 0;
  const targets = getSiteEnrollmentTargetsByCycle(row);
  const value = targets[currentEnrollmentPlanCycle.value]?.[key];
  if (value === null || value === undefined) return 0;
  return normalizeEnrollmentTarget(value);
};
const displaySiteEnrollmentPeriodTarget = (row: SiteEnrollmentPlanDraft, key: string | null): string => {
  if (!key) return "-";
  const targets = getSiteEnrollmentTargetsByCycle(row);
  const value = targets[currentEnrollmentPlanCycle.value]?.[key];
  if (value === null || value === undefined) return "-";
  return String(normalizeEnrollmentTarget(value));
};
const setSiteEnrollmentPeriodTarget = (rowIndex: number, key: string | null, value: number | null) => {
  if (!key) return;
  const row = setupDraft.siteEnrollmentPlans[rowIndex];
  if (!row) return;
  const targets = getSiteEnrollmentTargetsByCycle(row);
  const nextCycleTargets = { ...(targets[enrollmentPlanCycle.value] || {}) };
  if (value === null || value === undefined) {
    delete nextCycleTargets[key];
  } else {
    nextCycleTargets[key] = normalizeEnrollmentTarget(value);
  }
  targets[enrollmentPlanCycle.value] = nextCycleTargets;
  row.stageBreakdown = buildSiteEnrollmentStageBreakdown(targets);
  row.target = sumSiteEnrollmentTargetForCurrentCycle(targets);
};
const clearSiteEnrollmentPeriodPlan = (rowIndex: number) => {
  const row = setupDraft.siteEnrollmentPlans[rowIndex];
  if (!row) return;
  const targets = getSiteEnrollmentTargetsByCycle(row);
  const scopedKeys = new Set(currentEnrollmentSlots.value.map((item) => item.key));
  const nextCycleTargets = { ...(targets[enrollmentPlanCycle.value] || {}) };
  scopedKeys.forEach((key) => {
    delete nextCycleTargets[key];
  });
  targets[enrollmentPlanCycle.value] = nextCycleTargets;
  row.stageBreakdown = buildSiteEnrollmentStageBreakdown(targets);
  row.target = sumSiteEnrollmentTargetForCurrentCycle(targets);
};
const quickFillSiteEnrollmentPeriodPlan = (rowIndex: number) => {
  const row = setupDraft.siteEnrollmentPlans[rowIndex];
  if (!row) return;
  const slots = currentEnrollmentSlots.value;
  if (!slots.length) {
    ElMessage.warning("请先填写有效的计划入组日期范围");
    return;
  }
  const rowTarget = normalizeEnrollmentTarget(row.target);
  const base = Math.floor(rowTarget / slots.length);
  let remainder = rowTarget - base * slots.length;
  const targets = getSiteEnrollmentTargetsByCycle(row);
  const nextCycleTargets = { ...(targets[enrollmentPlanCycle.value] || {}) };
  slots.forEach((slot) => {
    const extra = remainder > 0 ? 1 : 0;
    nextCycleTargets[slot.key] = base + extra;
    if (remainder > 0) remainder -= 1;
  });
  targets[enrollmentPlanCycle.value] = nextCycleTargets;
  row.stageBreakdown = buildSiteEnrollmentStageBreakdown(targets);
  row.target = sumSiteEnrollmentTargetForCurrentCycle(targets);
};
const applyEnrollmentStageFromDraft = () => {
  const parsed = parseEnrollmentStageBreakdown(setupDraft.enrollmentPlan.stageBreakdown);
  enrollmentStageSyncingFromModel.value = true;
  enrollmentPlanCycle.value = parsed.cycle;
  enrollmentTargetsByCycle.value = {
    ...createEmptyEnrollmentTargetsByCycle(),
    ...parsed.targetsByCycle,
  };
  enrollmentStageSyncingFromModel.value = false;
};

const getFieldError = (fieldPath: string): string => {
  return serverFieldErrors.value[fieldPath] || "";
};

const extractValidationErrors = (error: any): SetupValidationError[] => {
  const detail = error?.response?.data?.detail;
  const list = detail?.errors || error?.response?.data?.errors;
  if (!Array.isArray(list)) return [];
  return list
    .map((item: any) => ({
      field: String(item?.field || ""),
      message: String(item?.message || ""),
    }))
    .filter((item: SetupValidationError) => item.field && item.message);
};

const markValidationErrors = (errors: SetupValidationError[]) => {
  const next: Record<string, string> = {};
  errors.forEach((item) => {
    next[item.field] = item.message;
  });
  serverFieldErrors.value = next;
};

const getApiErrorMessage = (error: any, fallback: string): string => {
  const detail = error?.response?.data?.detail;
  const errors = Array.isArray(detail?.errors) ? detail.errors : [];
  const firstValidationMessage = typeof errors[0]?.message === "string" ? errors[0].message : "";
  if (firstValidationMessage) {
    if (typeof detail?.message === "string" && detail.message.trim()) {
      return `${detail.message}：${firstValidationMessage}`;
    }
    return firstValidationMessage;
  }
  if (typeof detail === "string") return detail;
  if (typeof detail?.message === "string") return detail.message;
  if (typeof error?.response?.data?.message === "string") return error.response.data.message;
  return fallback;
};

const focusFirstValidationField = (errors: SetupValidationError[]) => {
  const first = errors[0];
  if (!first) return;
  const parsed = parseFieldPath(first.field);
  if (parsed && setupSectionStepMap[parsed.section] !== undefined) {
    activeStep.value = setupSectionStepMap[parsed.section];
  } else if (first.field.startsWith("enrollmentPlan.")) {
    activeStep.value = 2;
  }
  requestAnimationFrame(() => {
    const target = document.querySelector(`[data-field-path="${first.field}"]`) as HTMLElement | null;
    target?.scrollIntoView({ behavior: "smooth", block: "center" });
  });
};

const handleValidationFailure = (error: any, fallback: string): boolean => {
  const validationErrors = extractValidationErrors(error);
  if (!validationErrors.length) return false;
  markValidationErrors(validationErrors);
  focusFirstValidationField(validationErrors);
  ElMessage.error(getApiErrorMessage(error, fallback));
  return true;
};

const stepHasErrors = (stepIndex: number): boolean => {
  return Object.keys(serverFieldErrors.value).some((path) => {
    const parsed = parseFieldPath(path);
    if (parsed) return setupSectionStepMap[parsed.section] === stepIndex;
    if (path.startsWith("enrollmentPlan.")) return stepIndex === 2;
    if (path.startsWith("projectMilestones")) return stepIndex === 1;
    if (path.startsWith("siteMilestones")) return stepIndex === 3;
    if (path.startsWith("siteEnrollmentPlans")) return stepIndex === 4;
    if (path.startsWith("centerConfirm")) return stepIndex === 5;
    return false;
  });
};

const isStepCompleted = (stepIndex: number): boolean => {
  if (stepHasErrors(stepIndex)) return false;
  if (stepIndex === 0) {
    return Boolean((form.value.name || "").trim() && (form.value.code || "").trim());
  }
  if (stepIndex === 1) {
    return (
      setupDraft.projectMilestones.length > 0 &&
      setupDraft.projectMilestones.every((item) => {
        const startDate = resolveProjectMilestoneStart(item);
        const endDate = resolveProjectMilestoneEnd(item);
        return (item.name || "").trim().length > 0 && !!startDate && !!endDate && startDate <= endDate;
      })
    );
  }
  if (stepIndex === 2) {
    const plan = setupDraft.enrollmentPlan;
    const { start: projectPlanStart, end: projectPlanEnd } = getProjectPlanWindow();
    const enrollmentStart = normalizePlanDate(plan.startDate);
    const enrollmentEnd = normalizePlanDate(plan.endDate);
    if (!(plan.totalTarget >= 0 && enrollmentStart && enrollmentEnd && enrollmentStart <= enrollmentEnd)) return false;
    if (projectPlanStart && enrollmentStart < projectPlanStart) return false;
    if (projectPlanEnd && enrollmentStart > projectPlanEnd) return false;
    if (projectPlanStart && enrollmentEnd < projectPlanStart) return false;
    if (projectPlanEnd && enrollmentEnd > projectPlanEnd) return false;
    return true;
  }
  if (stepIndex === 3) {
    const { start: planStart, end: planEnd } = getProjectPlanWindow();
    return (
      setupDraft.siteMilestones.length > 0 &&
      setupDraft.siteMilestones.every((item) => {
        const planDate = normalizePlanDate(item.planDate);
        if (!((item.milestone || "").trim().length > 0 && planDate)) return false;
        if (planStart && planDate < planStart) return false;
        if (planEnd && planDate > planEnd) return false;
        return true;
      })
    );
  }
  if (stepIndex === 4) {
    const { start: planStart, end: planEnd } = getProjectPlanWindow();
    const siteIdSet = new Set<string>();
    return (
      setupDraft.siteEnrollmentPlans.length > 0 &&
      setupDraft.siteEnrollmentPlans.every((item) => {
        const siteId = normalizeSiteId(item.siteId);
        if (!siteId) return false;
        if (siteIdSet.has(siteId)) return false;
        siteIdSet.add(siteId);
        const startDate = normalizePlanDate(item.startDate);
        const endDate = normalizePlanDate(item.endDate);
        if (!(item.target >= 0 && startDate && endDate && startDate <= endDate)) return false;
        if (planStart && startDate < planStart) return false;
        if (planEnd && startDate > planEnd) return false;
        if (planStart && endDate < planStart) return false;
        if (planEnd && endDate > planEnd) return false;
        return true;
      })
    );
  }
  if (stepIndex === 5) {
    return true;
  }
  return false;
};
const incompleteSteps = computed(() => steps.map((step, index) => ({ index, label: step.label })).filter((item) => !isStepCompleted(item.index)));

const statusLabel = (status: string): string => {
  if (status === "ACTIVE") return TEXT.enums.projectStatus.ACTIVE;
  if (status === "CLOSED") return TEXT.enums.projectStatus.CLOSED;
  return TEXT.enums.projectStatus.DRAFT;
};

const toDisplayNumber = (value: number | null | undefined): string => {
  if (value === null || value === undefined) return "-";
  return String(value);
};

const normalizeSiteId = (siteId: string | null | undefined): string => String(siteId || "").trim();
const getSiteOptionLabel = (site: Partial<Site> | null | undefined): string => {
  const name = String(site?.name || "").trim();
  if (name) return name;
  const siteId = normalizeSiteId((site?.id as string | undefined) || "");
  if (siteId) return `中心-${siteId.slice(0, 8)}`;
  return "未命名中心";
};
const siteSelectOptions = computed(() =>
  siteOptions.value
    .map((site) => ({
      id: normalizeSiteId(String(site.id || "")),
      label: getSiteOptionLabel(site),
    }))
    .filter((site) => Boolean(site.id))
);
const getSiteName = (siteId: string): string => {
  const normalizedSiteId = normalizeSiteId(siteId);
  const matched = siteOptions.value.find((s) => normalizeSiteId(String(s.id || "")) === normalizedSiteId);
  return getSiteOptionLabel(matched || { id: normalizedSiteId, name: "" });
};
const normalizeOwnerDisplay = (value: string | null | undefined): string => {
  if (!value) return "-";
  return value
    .split(/[,\uFF0C;\uFF1B]/)
    .map((item) => item.trim().replace(/^[\[\]"'`]+|[\[\]"'`]+$/g, ""))
    .filter(Boolean)
    .map((item) => memberDisplayMap.value[item] || item)
    .join("、") || "-";
};
const projectMilestoneOwnerOptions = computed(() => {
  const options = [...projectMemberOptions.value];
  const currentOwner = String(projectMilestoneEditorForm.owner || "").trim();
  if (currentOwner && !options.some((option) => option.value === currentOwner)) {
    options.unshift({
      value: currentOwner,
      label: normalizeOwnerDisplay(currentOwner),
    });
  }
  return options;
});
const siteMilestoneOwnerOptions = computed(() => {
  const options = [...projectMemberOptions.value];
  const currentOwner = String(siteMilestoneEditorForm.owner || "").trim();
  if (currentOwner && !options.some((option) => option.value === currentOwner)) {
    options.unshift({
      value: currentOwner,
      label: normalizeOwnerDisplay(currentOwner),
    });
  }
  return options;
});
const siteEnrollmentPlanMap = computed(() => {
  const map = new Map<string, SiteEnrollmentPlanDraft>();
  currentSetupDraft.value.siteEnrollmentPlans.forEach((row) => {
    const siteId = normalizeSiteId(row.siteId);
    if (!siteId) return;
    map.set(siteId, row);
  });
  return map;
});
const centerConfirmDisplayRows = computed(() => {
  return siteOptions.value.map((site) => {
    const siteId = normalizeSiteId(site.id);
    const plan = siteEnrollmentPlanMap.value.get(siteId);
    return {
      siteId,
      siteName: getSiteOptionLabel(site),
      city: site.city || "",
      piName: site.pi_name || "",
      ownerDisplay: normalizeOwnerDisplay(site.contact),
      target: normalizeEnrollmentTarget(plan?.target),
      isActive: site.is_active !== false,
      startDate: plan?.startDate || "",
      endDate: plan?.endDate || "",
    };
  });
});
const selectedSiteEnrollmentPlanSiteId = ref("");
const siteEnrollmentSelectableSites = computed(() => {
  const map = new Map<string, Site>();
  siteOptions.value.forEach((site) => {
    const siteId = normalizeSiteId(site.id);
    if (!siteId) return;
    map.set(siteId, {
      ...site,
      id: siteId,
      name: getSiteOptionLabel(site),
    });
  });
  currentSetupDraft.value.siteEnrollmentPlans.forEach((row) => {
    const siteId = normalizeSiteId(row.siteId);
    if (!siteId || map.has(siteId)) return;
    map.set(siteId, {
      id: siteId,
      study_id: project.value?.id || "",
      name: (row.siteName || "").trim() || `中心-${siteId.slice(0, 8)}`,
      city: "",
      pi_name: "",
      contact: "",
      enrollment_target: row.target || 0,
      is_active: true,
    });
  });
  return Array.from(map.values());
});
const selectedSiteEnrollmentPlanIndex = computed(() => {
  const selectedSiteId = normalizeSiteId(selectedSiteEnrollmentPlanSiteId.value);
  if (!selectedSiteId) return -1;
  return currentSetupDraft.value.siteEnrollmentPlans.findIndex((row) => normalizeSiteId(row.siteId) === selectedSiteId);
});
const selectedSiteEnrollmentPlan = computed(() => {
  const index = selectedSiteEnrollmentPlanIndex.value;
  if (index < 0) return null;
  return currentSetupDraft.value.siteEnrollmentPlans[index] || null;
});
const selectedSitePlanName = computed(() => {
  const nameFromPlan = (selectedSiteEnrollmentPlan.value?.siteName || "").trim();
  if (nameFromPlan) return nameFromPlan;
  const selectedSiteId = normalizeSiteId(selectedSiteEnrollmentPlanSiteId.value);
  if (!selectedSiteId) return "当前中心";
  const matched = siteEnrollmentSelectableSites.value.find((site) => normalizeSiteId(site.id) === selectedSiteId);
  const name = (matched?.name || "").trim();
  return name || "当前中心";
});
const selectedSitePlanTarget = computed(() => normalizeEnrollmentTarget(selectedSiteEnrollmentPlan.value?.target));
const selectedSiteCyclePlannedCount = computed(() => {
  const row = selectedSiteEnrollmentPlan.value;
  if (!row) return 0;
  return sumSiteEnrollmentTargetForCurrentCycle(getSiteEnrollmentTargetsByCycle(row));
});
const selectedSiteCyclePendingCount = computed(() =>
  Math.max(selectedSitePlanTarget.value - selectedSiteCyclePlannedCount.value, 0)
);
const syncSelectedSiteEnrollmentPlanSiteId = () => {
  const selectedSiteId = normalizeSiteId(selectedSiteEnrollmentPlanSiteId.value);
  if (selectedSiteId && siteEnrollmentSelectableSites.value.some((site) => normalizeSiteId(site.id) === selectedSiteId)) {
    return;
  }
  const firstConfiguredSiteId =
    currentSetupDraft.value.siteEnrollmentPlans.map((row) => normalizeSiteId(row.siteId)).find((siteId) => Boolean(siteId)) ||
    "";
  if (firstConfiguredSiteId) {
    selectedSiteEnrollmentPlanSiteId.value = firstConfiguredSiteId;
    return;
  }
  selectedSiteEnrollmentPlanSiteId.value = normalizeSiteId(siteEnrollmentSelectableSites.value[0]?.id || "");
};
const createSelectedSiteEnrollmentPlan = () => {
  if (!canMutateDraft()) return;
  const selectedSiteId = normalizeSiteId(selectedSiteEnrollmentPlanSiteId.value);
  if (!selectedSiteId) {
    ElMessage.warning("请先选择中心");
    return;
  }
  if (selectedSiteEnrollmentPlanIndex.value >= 0) return;
  setupDraft.siteEnrollmentPlans.push({
    id: makeId(),
    siteId: selectedSiteId,
    siteName: getSiteName(selectedSiteId),
    target: 0,
    startDate: setupDraft.enrollmentPlan.startDate || "",
    endDate: setupDraft.enrollmentPlan.endDate || "",
    note: "",
    stageBreakdown: "",
  });
};
const getSiteEnrollmentEditorIndex = () => getEditingIndex(siteEnrollmentEditor, setupDraft.siteEnrollmentPlans.length);
const isSiteEnrollmentDuplicate = (siteId: string, excludeIndex: number): boolean => {
  const normalizedSiteId = normalizeSiteId(siteId);
  if (!normalizedSiteId) return false;
  return setupDraft.siteEnrollmentPlans.some(
    (row, rowIndex) => rowIndex !== excludeIndex && normalizeSiteId(row.siteId) === normalizedSiteId
  );
};
const isSiteEnrollmentSiteTaken = (siteId: string): boolean => {
  const editingIndex = getSiteEnrollmentEditorIndex();
  return isSiteEnrollmentDuplicate(siteId, editingIndex);
};
watch(
  [siteOptions, () => currentSetupDraft.value.siteEnrollmentPlans.map((row) => normalizeSiteId(row.siteId)).join("|")],
  syncSelectedSiteEnrollmentPlanSiteId,
  { immediate: true }
);

const createDefaultSetupDraft = (_sites: Site[]): SetupConfigDraft => {
  return {
    projectInfo: buildProjectPublishSnapshot(),
    projectMilestones: [],
    enrollmentPlan: {
      totalTarget: 0,
      startDate: "",
      endDate: "",
      monthlyGoalNote: "",
      stageBreakdown: "",
    },
    siteMilestones: [],
    siteEnrollmentPlans: [],
      centerConfirm: [],
  };
};

const normalizeSiteEnrollmentPlans = (rows: SiteEnrollmentPlanDraft[]): SiteEnrollmentPlanDraft[] =>
  rows.map((row) => ({
    ...row,
    stageBreakdown: row.stageBreakdown || "",
  }));

const isSetupDraftShape = (value: unknown): value is SetupConfigDraft => {
  if (!value || typeof value !== "object") return false;
  const data = value as Record<string, unknown>;
  return (
    isProjectPublishSnapshotShape(data.projectInfo) &&
    Array.isArray(data.projectMilestones) &&
    !!data.enrollmentPlan &&
    Array.isArray(data.siteMilestones) &&
    Array.isArray(data.siteEnrollmentPlans) &&
    Array.isArray(data.centerConfirm)
  );
};

const isProjectPublishSnapshotShape = (value: unknown): value is ProjectPublishSnapshot => {
  if (!value || typeof value !== "object") return false;
  const data = value as Record<string, unknown>;
  return (
    typeof data.code === "string" &&
    typeof data.name === "string" &&
    typeof data.project_full_name === "string" &&
    typeof data.status === "string"
  );
};

function isProjectPublishSnapshotEmpty(snapshot: ProjectPublishSnapshot): boolean {
  return JSON.stringify(snapshot) === JSON.stringify(emptyProjectInfoDraft());
}

const applyProjectInfoDraft = (snapshot: ProjectPublishSnapshot) => {
  suppressEnrollmentFieldSync.value = true;
  form.value.code = snapshot.code || "";
  form.value.name = snapshot.name || "";
  form.value.project_full_name = snapshot.project_full_name || "";
  form.value.sponsor = snapshot.sponsor || "";
  form.value.protocol_no = snapshot.protocol_no || "";
  form.value.lead_unit = snapshot.lead_unit || "";
  form.value.principal_investigator = snapshot.principal_investigator || "";
  form.value.main_pm = snapshot.main_pm || "";
  form.value.research_analysis = snapshot.research_analysis || "";
  form.value.research_product = snapshot.research_product || "";
  form.value.control_product = snapshot.control_product || "";
  form.value.indication = snapshot.indication || "";
  form.value.research_population = snapshot.research_population || "";
  form.value.research_design = snapshot.research_design || "";
  form.value.plan_start_date = snapshot.plan_start_date || "";
  form.value.plan_end_date = snapshot.plan_end_date || "";
  form.value.planned_site_count = snapshot.planned_site_count ?? null;
  form.value.planned_enrollment_count = snapshot.planned_enrollment_count ?? null;
  form.value.status = snapshot.status || "DRAFT";
  form.value.visit_schedule = normalizeVisitSchedule(snapshot.visit_schedule);
  suppressEnrollmentFieldSync.value = false;
};

const applySetupDraft = (draft: SetupConfigDraft) => {
  suppressDraftWatch.value = true;
  const projectInfo = isProjectPublishSnapshotShape(draft.projectInfo) ? clone(draft.projectInfo) : buildProjectPublishSnapshot();
  setupDraft.projectInfo = projectInfo;
  setupDraft.projectMilestones = normalizeProjectMilestoneRows(clone(draft.projectMilestones));
  setupDraft.enrollmentPlan = clone(draft.enrollmentPlan);
  setupDraft.siteMilestones = clone(draft.siteMilestones);
  setupDraft.siteEnrollmentPlans = normalizeSiteEnrollmentPlans(clone(draft.siteEnrollmentPlans));
  setupDraft.centerConfirm = clone(draft.centerConfirm);
  applyProjectInfoDraft(projectInfo);
  formBaselineSnapshot.value = serializeFormForCompare();
  suppressDraftWatch.value = false;
};

const loadLocalSetupDraft = (): LocalDraftLoadResult<SetupConfigDraft> | null => {
  try {
    const raw = localStorage.getItem(storageKey.value);
    if (!raw) return null;
    return parseLocalDraftEnvelope(
      raw,
      (candidate) => (isSetupDraftShape(candidate) ? clone(candidate) : null),
      setupRevision.value
    );
  } catch {
    return null;
  }
};

const persistSetupDraftToLocal = () => {
  try {
    const savedAt = nowString();
    localStorage.setItem(
      storageKey.value,
      JSON.stringify({
        version: DRAFT_VERSION,
        data: setupDraftForPersistence(),
        savedAt,
        savedAtMs: nowMs(),
        ttlMs: LOCAL_DRAFT_TTL_MS,
        serverVersionAtCache: setupRevision.value,
      })
    );
    setupLocalDraftSavedAt.value = savedAt;
    setupSaveMeta.value = {
      updatedAt: savedAt,
      savedBy: authStore.user?.full_name || authStore.user?.username || authStore.user?.email || "当前用户",
      serverSynced: false,
    };
  } catch {
    // ignore local storage errors
  }
};

const cacheSetupDraftToLocal = () => {
  try {
    const savedAt = nowString();
    localStorage.setItem(
      storageKey.value,
      JSON.stringify({
        version: DRAFT_VERSION,
        data: setupDraftForPersistence(),
        savedAt,
        savedAtMs: nowMs(),
        ttlMs: LOCAL_DRAFT_TTL_MS,
        serverVersionAtCache: setupRevision.value,
      })
    );
    setupLocalDraftSavedAt.value = savedAt;
  } catch {
    // ignore local storage errors
  }
};

const clearLocalProjectDraft = () => {
  try {
    localStorage.removeItem(projectDraftStorageKey.value);
  } catch {
    // ignore local storage errors
  }
};
const clearLocalSetupDraft = () => {
  try {
    localStorage.removeItem(storageKey.value);
    setupLocalDraftSavedAt.value = "";
  } catch {
    // ignore local storage errors
  }
};
const confirmRestoreLocalDraft = async (
  title: string,
  savedAt: string,
  options?: { expired?: boolean; conflict?: boolean }
): Promise<boolean> => {
  const tips: string[] = [`检测到本地草稿（保存时间：${savedAt || "-"})。`];
  if (options?.expired) {
    tips.push("该草稿已超过保留周期，可能过期。");
  }
  if (options?.conflict) {
    tips.push("服务端版本已更新，直接恢复可能产生冲突。");
  }
  tips.push("是否恢复并继续编辑？");
  try {
    await ElMessageBox.confirm(tips.join("\n"), title, {
      type: "warning",
      confirmButtonText: "恢复草稿",
      cancelButtonText: "忽略草稿",
    });
    return true;
  } catch {
    return false;
  }
};

const loadSetupDraftFromServer = async (): Promise<SetupConfigDraft | null> => {
  if (!project.value) return null;
  try {
    const { data } = await fetchSetupConfig(project.value.id);
    if (!isSetupDraftShape(data?.data)) return null;
    const draft = {
      ...clone(data.data),
      projectInfo: isProjectPublishSnapshotShape(data.data.projectInfo) ? clone(data.data.projectInfo) : buildProjectPublishSnapshot(),
    };
    applySetupResponseMeta(data);
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    return draft;
  } catch {
    return null;
  }
};

const resolveSavedByDisplay = (savedBy?: string | null): string => {
  if (!savedBy) return "-";
  if (savedBy === authStore.user?.id) {
    return authStore.user?.full_name || authStore.user?.username || authStore.user?.email || "当前用户";
  }
  return savedBy;
};

const setupModuleLabels: Array<{ key: keyof SetupConfigDraft; label: string }> = [
  { key: "projectMilestones", label: "项目里程碑" },
  { key: "enrollmentPlan", label: "项目入组计划" },
  { key: "siteMilestones", label: "中心里程碑" },
  { key: "siteEnrollmentPlans", label: "中心入组计划" },
  { key: "centerConfirm", label: "中心确认" },
];
const emptySetupDraft: SetupConfigDraft = {
  projectInfo: emptyProjectInfoDraft(),
  projectMilestones: [],
  enrollmentPlan: {
    totalTarget: 0,
    startDate: "",
    endDate: "",
    monthlyGoalNote: "",
    stageBreakdown: "",
  },
  siteMilestones: [],
  siteEnrollmentPlans: [],
  centerConfirm: [],
};
const publishCompareBase = computed<SetupConfigDraft>(() => clone(publishedSetupDraft.value || emptySetupDraft));
const isFirstPublish = computed(() => !publishedSetupDraft.value);
const hasBranchPublishDiff = computed(() => {
  const draftBranch = (setupCurrentBranchName.value || "main").trim() || "main";
  const publishedBranch = (setupPublishedBranchName.value || "main").trim() || "main";
  return draftBranch !== publishedBranch;
});
const projectPublishCompareBase = computed<ProjectPublishSnapshot | null>(() => {
  const base = resolveProjectPublishCompareBase(publishedProjectSnapshot.value, projectPublishCompareFallback.value);
  if (base) return clone(base);
  return isFirstPublish.value ? emptyProjectInfoDraft() : null;
});
const hasProjectPublishDiff = computed(() => {
  return hasProjectSnapshotDiff(buildProjectPublishSnapshot(), projectPublishCompareBase.value);
});
const canStrictlyDetectNoDiff = computed(() => Boolean(!isFirstPublish.value && publishedProjectSnapshot.value));
const hasPublishableDiff = computed(() => hasBranchPublishDiff.value || publishDiffLines.value.length > 0 || hasProjectPublishDiff.value);
const publishDiffLines = computed(() => {
  const lines = buildSetupModuleDiffLines(setupDraft, publishCompareBase.value, setupModuleLabels);
  if (hasBranchPublishDiff.value) {
    lines.unshift(`发布分支差异：草稿 ${setupCurrentBranchName.value || "main"}，当前发布 ${setupPublishedBranchName.value || "main"}`);
  }
  if (hasProjectPublishDiff.value) {
    lines.unshift("项目信息存在差异");
  }
  return lines;
});
type DiffRow = SetupDiffRow;
const projectPublishDiffRows = computed<DiffRow[]>(() =>
  buildProjectDiffRows(buildProjectPublishSnapshot(), projectPublishCompareBase.value, serializeDiffValue).map(
    normalizeDiffRowForBusiness
  )
);
const publishFieldDiffRows = computed(() => [...projectPublishDiffRows.value, ...buildReadableDiffRows(setupDraft, publishCompareBase.value)]);
const publishDiffModuleSummary = computed(() => {
  const grouped = new Map<string, number>();
  publishFieldDiffRows.value.forEach((row) => {
    grouped.set(row.moduleLabel, (grouped.get(row.moduleLabel) || 0) + 1);
  });
  return Array.from(grouped.entries()).map(([module, count]) => ({ module, count }));
});

const normalizeDiffRowForBusiness = (row: DiffRow): DiffRow => {
  if (row.path.includes("负责人")) {
    return {
      ...row,
      localValue: row.localValue === "未填写" ? row.localValue : normalizeOwnerDisplay(row.localValue),
      serverValue: row.serverValue === "未填写" ? row.serverValue : normalizeOwnerDisplay(row.serverValue),
    };
  }
  return row;
};

const buildReadableDiffRows = (localDraft: SetupConfigDraft | null, serverDraft: SetupConfigDraft | null): DiffRow[] =>
  buildSetupReadableDiffRows(localDraft, serverDraft, setupModuleLabels).map(normalizeDiffRowForBusiness);

const applySetupResponseMeta = (data: StudySetupConfigResponse) => {
  setupRevision.value = typeof data.version === "number" ? data.version : setupRevision.value;
  setupCurrentBranchName.value = String(data.current_branch_name || setupCurrentBranchName.value || "main");
  if (typeof data.current_published_version_label === "string" && data.current_published_version_label.trim()) {
    setupPublishedVersion.value = data.current_published_version_label.trim();
  }
  setupActiveDraftBaseVersion.value =
    typeof data.active_branch_base_version_label === "string" ? data.active_branch_base_version_label.trim() : "";
  setupPublishStatus.value = data.publish_status || "DRAFT";
  setupPublishedAt.value = formatDisplayTime(data.published_at);
  publishedSetupDraft.value = isSetupDraftShape(data?.published_data) ? clone(data.published_data) : null;
  const previousProjectSnapshot = publishedProjectSnapshot.value ? clone(publishedProjectSnapshot.value) : null;
  const apiProjectSnapshot = isProjectPublishSnapshotShape(data?.published_project_snapshot)
    ? clone(data.published_project_snapshot)
    : null;
  const resolved = resolvePublishedProjectSnapshot({
    publishStatus: setupPublishStatus.value,
    apiProjectSnapshot,
    previousSnapshot: previousProjectSnapshot,
    projectSnapshotAtLoad: projectSnapshotAtLoad.value ? clone(projectSnapshotAtLoad.value) : null,
    currentProjectSnapshot: buildProjectPublishSnapshot(),
  });
  publishedProjectSnapshot.value = resolved.snapshot ? clone(resolved.snapshot) : null;
  if (resolved.clearFallback) {
    projectPublishCompareFallback.value = null;
  }
};

const refreshSetupDraftFromServer = async (options?: { silent?: boolean }) => {
  const latest = await loadSetupDraftFromServer();
  if (!latest) return false;
  applySetupDraft(latest);
  clearSetupValidationErrors();
  markSetupServerSynced();
  if (!options?.silent) {
    ElMessage.info("已刷新服务端最新配置");
  }
  return true;
};

const persistSetupDraft = async (
  showMessage = false,
  options?: { forceDraft?: boolean }
): Promise<{ serverSynced: boolean; retryable: boolean }> => {
  if (!project.value) return { serverSynced: false, retryable: false };
  const payload: StudySetupConfigUpsertPayload = {
    expected_version: setupRevision.value,
    data: setupDraftForPersistence(),
    force_draft: Boolean(options?.forceDraft),
  };
  try {
    const { data } = await upsertSetupConfig(project.value.id, payload);
    applySetupResponseMeta(data);
    clearSetupValidationErrors();
    markSetupServerSynced();
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    return { serverSynced: true, retryable: false };
  } catch (e: any) {
    if (handleValidationFailure(e, "配置保存失败，请修正后重试")) {
      persistSetupDraftToLocal();
      setupDirtySinceLastPersist.value = true;
      return { serverSynced: false, retryable: false };
    }
    persistSetupDraftToLocal();
    setupDirtySinceLastPersist.value = true;
    if (e?.response?.status === 409) {
      ElMessage.warning("配置已更新，正在刷新最新版本");
      await refreshSetupDraftFromServer({ silent: true });
      return { serverSynced: false, retryable: false };
    }
    if (e?.response?.status === 403) {
      if (project.value) {
        project.value.is_locked = true;
      }
      isEditing.value = false;
    }
    if (showMessage) {
      if (e?.response?.status === 403) {
        ElMessage.warning("项目已锁定，配置未同步到服务器，仅保留本地草稿");
      } else {
        ElMessage.warning(getApiErrorMessage(e, "服务器同步失败，已保存为本地草稿"));
      }
    }
    return { serverSynced: false, retryable: !e?.response?.status };
  }
};

const persistSetupDraftWithRetry = async (
  showMessage = false,
  options?: { forceDraft?: boolean }
): Promise<{ serverSynced: boolean }> => {
  for (let attempt = 0; attempt <= SAVE_RETRY_DELAYS_MS.length; attempt += 1) {
    const result = await persistSetupDraft(showMessage && attempt === SAVE_RETRY_DELAYS_MS.length, options);
    if (result.serverSynced) {
      return { serverSynced: true };
    }
    if (!result.retryable || attempt === SAVE_RETRY_DELAYS_MS.length) {
      return { serverSynced: false };
    }
    await sleep(SAVE_RETRY_DELAYS_MS[attempt]);
  }
  return { serverSynced: false };
};

const schedulePersistDraft = () => {
  if (autoSaveTimer.value) {
    window.clearTimeout(autoSaveTimer.value);
  }
  autoSaveTimer.value = window.setTimeout(() => {
    autoSaveTimer.value = undefined;
    persistSetupDraftToLocal();
  }, 500);
};

watch(
  () => setupDraft.enrollmentPlan.stageBreakdown,
  () => {
    if (enrollmentStageSyncingToModel.value) return;
    applyEnrollmentStageFromDraft();
  },
  { immediate: true }
);

watch(
  [enrollmentPlanCycle, enrollmentTargetsByCycle],
  () => {
    if (enrollmentStageSyncingFromModel.value) return;
    if (!draftReady.value || !isEditing.value || isPublishedView.value || !canEditSetup.value) return;
    const nextStageBreakdown = buildEnrollmentStageBreakdown({
      cycle: enrollmentPlanCycle.value,
      targetsByCycle: enrollmentTargetsByCycle.value,
    });
    if (nextStageBreakdown === setupDraft.enrollmentPlan.stageBreakdown) return;
    enrollmentStageSyncingToModel.value = true;
    setupDraft.enrollmentPlan.stageBreakdown = nextStageBreakdown;
    enrollmentStageSyncingToModel.value = false;
  },
  { deep: true }
);

watch(
  setupDraft,
  () => {
    if (!draftReady.value || suppressDraftWatch.value || !canEditSetup.value) return;
    const changed = refreshSetupDirtyState();
    if (!changed) {
      clearPendingAutoSave();
      return;
    }
    schedulePersistDraft();
  },
  { deep: true }
);

watch(
  () => form.value.planned_enrollment_count,
  (next) => {
    if (!draftReady.value || suppressEnrollmentFieldSync.value) return;
    const normalized = normalizeEnrollmentTarget(next);
    if (
      setupDraft.enrollmentPlan.totalTarget === normalized &&
      form.value.planned_enrollment_count === normalized
    ) {
      return;
    }
    suppressEnrollmentFieldSync.value = true;
    form.value.planned_enrollment_count = normalized;
    setupDraft.enrollmentPlan.totalTarget = normalized;
    suppressEnrollmentFieldSync.value = false;
  }
);

watch(
  () => setupDraft.enrollmentPlan.totalTarget,
  (next) => {
    if (!draftReady.value || suppressEnrollmentFieldSync.value) return;
    const normalized = normalizeEnrollmentTarget(next);
    if (
      form.value.planned_enrollment_count === normalized &&
      setupDraft.enrollmentPlan.totalTarget === normalized
    ) {
      return;
    }
    suppressEnrollmentFieldSync.value = true;
    setupDraft.enrollmentPlan.totalTarget = normalized;
    form.value.planned_enrollment_count = normalized;
    suppressEnrollmentFieldSync.value = false;
  }
);

watch(setupViewMode, (mode) => {
  if (mode !== "draft") {
    isEditing.value = false;
    step1EditSection.value = "all";
  }
});

const syncForm = (data: Study | null) => {
  form.value.code = data?.code || "";
  form.value.name = data?.name || "";
  form.value.project_full_name = data?.project_full_name || "";
  form.value.short_name = "";
  form.value.sponsor = data?.sponsor || "";
  form.value.protocol_no = data?.protocol_no || "";
  form.value.lead_unit = data?.lead_unit || "";
  form.value.principal_investigator = data?.principal_investigator || "";
  form.value.main_pm = data?.main_pm || "";
  form.value.research_analysis = data?.research_analysis || "";
  form.value.research_product = data?.research_product || "";
  form.value.control_product = data?.control_product || "";
  form.value.indication = data?.indication || "";
  form.value.research_population = data?.research_population || "";
  form.value.research_design = data?.research_design || "";
  form.value.plan_start_date = data?.plan_start_date || "";
  form.value.plan_end_date = data?.plan_end_date || "";
  form.value.planned_site_count = data?.planned_site_count ?? null;
  form.value.planned_enrollment_count = data?.planned_enrollment_count ?? null;
  form.value.phase = data?.phase || "";
  form.value.status = data?.status || "DRAFT";
  form.value.scope = "国内";
  form.value.visit_schedule = normalizeVisitSchedule(data?.visit_schedule);
  formBaselineSnapshot.value = serializeFormForCompare();
};

const syncFormFromProjectWithoutSetupLink = (data: Study | null) => {
  suppressEnrollmentFieldSync.value = true;
  syncForm(data);
  suppressEnrollmentFieldSync.value = false;
};

const initializeSetupDraft = async () => {
  clearSetupValidationErrors();
  const serverDraft = await loadSetupDraftFromServer();
  if (serverDraft) {
    applySetupDraft(serverDraft);
    setupServerSnapshot.value = serializeSetupForCompare();
    markSetupSynced();
    cacheSetupDraftToLocal();
    draftReady.value = true;
    return;
  }

  const localDraft = loadLocalSetupDraft();
  if (localDraft) {
    if (!isAtomicSetupDraft(localDraft.data)) {
      clearLocalSetupDraft();
    } else {
      const shouldRestore = await confirmRestoreLocalDraft("恢复立项配置草稿", localDraft.savedAt, {
        expired: localDraft.expired,
        conflict: localDraft.conflict,
      });
      if (shouldRestore) {
        applySetupDraft(localDraft.data);
        setupDirtySinceLastPersist.value = true;
        setupLocalDraftSavedAt.value = localDraft.savedAt;
        setupSaveMeta.value = {
          updatedAt: localDraft.savedAt || nowString(),
          savedBy: authStore.user?.full_name || authStore.user?.username || authStore.user?.email || "当前用户",
          serverSynced: false,
        };
        draftReady.value = true;
        return;
      }
      clearLocalSetupDraft();
    }
  }

  applySetupDraft(createDefaultSetupDraft(siteOptions.value));
  setupServerSnapshot.value = "";
  markSetupSynced();
  draftReady.value = true;
  persistSetupDraftToLocal();
};

const loadMemberDisplayMap = async (studyId: string) => {
  const nextMap: Record<string, string> = {};
  const memberOptionMap = new Map<string, string>();
  try {
    const { data } = await listMembers(studyId, { limit: 500 });
    const rows = Array.isArray(data) ? data : (data as any).items || [];
    rows.forEach((row: any) => {
      if (row?.is_active === false) return;
      const userId = String(row?.user_id || row?.userId || "").trim();
      const memberId = String(row?.id || "").trim();
      if (!userId) return;
      const nestedUser = row?.user || {};
      const displayName =
        nestedUser?.full_name ||
        nestedUser?.username ||
        nestedUser?.email ||
        row?.full_name ||
        row?.username ||
        row?.email ||
        userId;
      const roleInStudy = String(row?.role_in_study || row?.roleInStudy || "").trim();
      const optionLabel = roleInStudy ? `${displayName}（${roleInStudy}）` : displayName;
      nextMap[userId] = displayName;
      if (memberId) nextMap[memberId] = displayName;
      if (!memberOptionMap.has(userId)) {
        memberOptionMap.set(userId, optionLabel);
      }
    });
  } catch {
    // Ignore member API errors.
  }
  memberDisplayMap.value = nextMap;
  projectMemberOptions.value = Array.from(memberOptionMap.entries())
    .map(([value, label]) => ({ value, label }))
    .sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
};

const loadProject = async () => {
  try {
    const projectId = route.params.projectId as string;
    projectPublishCompareFallback.value = null;

    const [studyRes, permRes] = await Promise.all([
      fetchStudyDetail(projectId),
      fetchMyApiEndpointPermissions(projectId, { suppressErrorMessage: true }).catch(() => ({ data: null })),
    ]);
    const { data } = studyRes;
    project.value = data as Study;
    projectPermissions.value = permRes.data as any;

    if (!hasSetupReadPermission.value) {
      ElMessage.warning("当前角色无权访问立项配置");
      router.replace("/admin/projects");
      return;
    }

    syncFormFromProjectWithoutSetupLink(project.value);
    projectSnapshotAtLoad.value = buildProjectPublishSnapshot();
    clearLocalProjectDraft();
    formBaselineSnapshot.value = serializeFormForCompare();
    if (setupSaveMeta.value.serverSynced) {
      setupDirtySinceLastPersist.value = false;
      if (autoSaveTimer.value) {
        window.clearTimeout(autoSaveTimer.value);
        autoSaveTimer.value = undefined;
      }
    }
    isEditing.value = false;

    const siteRes = await fetchSites(projectId, { limit: 500 });
    const list = Array.isArray(siteRes.data) ? siteRes.data : (siteRes.data as any).items || [];
    siteOptions.value = list;
    await loadMemberDisplayMap(projectId);
    await initializeSetupDraft();
    await loadSetupVersionHistory();
  } catch (e: any) {
    ElMessage.error(getApiErrorMessage(e, TEXT.modules.adminProjects.loadFailed));
  }
};

const refreshProjectDisplayAfterPublish = async () => {
  if (!project.value) return;
  try {
    const { data } = await fetchStudyDetail(project.value.id);
    project.value = data as Study;
    syncFormFromProjectWithoutSetupLink(project.value);
    formBaselineSnapshot.value = serializeFormForCompare();
    if (studyStore.currentStudy?.id === project.value.id) {
      studyStore.setCurrentStudy({ ...(studyStore.currentStudy as Study), ...project.value } as Study);
      await studyStore.loadCurrentStudyPermissions().catch(() => {});
    }
  } catch {
    // Ignore refresh errors to avoid breaking publish success flow.
  }
};

const startEdit = () => {
  if (!project.value) return;
  if (activeStep.value !== 0 && activeStep.value !== 2 && activeStep.value !== 4) return;
  if (isPublishedView.value) {
    ElMessage.info("当前为只读视图，请先切换到草稿后再编辑");
    return;
  }
  if (!canManageSetup.value) {
    ElMessage.warning("当前角色无权编辑立项配置");
    return;
  }
  if (project.value.is_locked) {
    ElMessage.warning("项目已锁定，无法编辑。");
    return;
  }
  clearSetupValidationErrors();
  if (activeStep.value === 0) {
    step1EditSection.value = infoTab.value === "summary" ? "summary" : "all";
  }
  formBaselineSnapshot.value = serializeFormForCompare();
  if (drawerCloseTimer.value) {
    window.clearTimeout(drawerCloseTimer.value);
    drawerCloseTimer.value = undefined;
  }
  drawerClosing.value = false;
  isEditing.value = true;
};

const startStep1SectionEdit = (section: Exclude<Step1EditSection, "all">) => {
  if (activeStep.value !== 0) return;
  if (section !== "summary") {
    infoTab.value = "basic";
  } else {
    infoTab.value = "summary";
  }
  startEdit();
  if (isEditing.value) {
    step1EditSection.value = section;
    if (section === "summary") {
      visitScheduleDialogVisible.value = true;
    }
  }
};

const cancelEdit = () => {
  applyProjectInfoDraft(setupDraft.projectInfo);
  isEditing.value = false;
  step1EditSection.value = "all";
  visitScheduleDialogVisible.value = false;
  clearSetupValidationErrors();
  formBaselineSnapshot.value = serializeFormForCompare();
};

const closeEditDrawer = () => {
  if (!isEditing.value || drawerClosing.value) return;
  if (visitScheduleDialogVisible.value) {
    cancelEdit();
    return;
  }
  drawerClosing.value = true;
  drawerCloseTimer.value = window.setTimeout(() => {
    cancelEdit();
    drawerClosing.value = false;
    drawerCloseTimer.value = undefined;
  }, 220);
};

const closeEditDrawerAfterSave = (onClosed?: () => void) => {
  if (!isEditing.value || drawerClosing.value) return;
  if (visitScheduleDialogVisible.value) {
    visitScheduleDialogVisible.value = false;
    isEditing.value = false;
    step1EditSection.value = "all";
    clearSetupValidationErrors();
    formBaselineSnapshot.value = serializeFormForCompare();
    onClosed?.();
    return;
  }
  drawerClosing.value = true;
  drawerCloseTimer.value = window.setTimeout(() => {
    isEditing.value = false;
    step1EditSection.value = "all";
    clearSetupValidationErrors();
    formBaselineSnapshot.value = serializeFormForCompare();
    drawerClosing.value = false;
    drawerCloseTimer.value = undefined;
    onClosed?.();
  }, 220);
};

const cancelVisitScheduleDialogEdit = () => {
  cancelEdit();
};

const saveVisitScheduleDialogEdit = async () => {
  if (!canEditSetup.value) return;
  drawerConfirmLoading.value = true;
  try {
    syncProjectInfoIntoSetupDraft();
    const setupChanged = refreshSetupDirtyState();
    if (setupChanged) {
      persistSetupDraftToLocal();
    } else {
      clearLocalSetupDraft();
      clearPendingAutoSave();
      markServerSyncedIfNoLocalChanges();
    }
    clearLocalProjectDraft();
    formBaselineSnapshot.value = serializeFormForCompare();
    visitScheduleDialogVisible.value = false;
    isEditing.value = false;
    step1EditSection.value = "all";
    clearSetupValidationErrors();
    ElMessage.success("访视计划已保存");
  } finally {
    drawerConfirmLoading.value = false;
  }
};

const handleDrawerConfirm = async () => {
  if (!canEditSetup.value) return;
  drawerConfirmLoading.value = true;
  try {
    let saved = false;
    if (activeStep.value === 0) {
      if (formRef.value) {
        await formRef.value.validate();
      }
      syncProjectInfoIntoSetupDraft();
      const setupChanged = refreshSetupDirtyState();
      if (setupChanged) {
        persistSetupDraftToLocal();
      } else {
        clearLocalSetupDraft();
        clearPendingAutoSave();
        markServerSyncedIfNoLocalChanges();
      }
        clearLocalProjectDraft();
      formBaselineSnapshot.value = serializeFormForCompare();
      saved = true;
    } else {
      const setupChanged = refreshSetupDirtyState();
      if (setupChanged) {
        persistSetupDraftToLocal();
      } else {
        clearLocalSetupDraft();
        clearPendingAutoSave();
        markServerSyncedIfNoLocalChanges();
      }
      saved = true;
    }
    if (!saved) return;
    closeEditDrawerAfterSave();
    ElMessage.success("当前步骤已保存");
  } finally {
    drawerConfirmLoading.value = false;
  }
};

const captureProjectCompareFallbackBeforeSave = (): ProjectPublishSnapshot | null => {
  if (
    !shouldCaptureProjectCompareFallback({
      hasProjectPendingChanges: hasFormUnsavedChanges.value,
      hasPublishedProjectSnapshot: Boolean(publishedProjectSnapshot.value),
      isFirstPublish: isFirstPublish.value,
    })
  ) {
    return null;
  }
  return buildProjectPublishSnapshotFromStudy(project.value);
};
const applyCapturedProjectCompareFallback = (snapshot: ProjectPublishSnapshot | null) => {
  if (!snapshot || publishedProjectSnapshot.value) return;
  projectPublishCompareFallback.value = clone(snapshot);
};

const saveConfigNow = async (): Promise<boolean> => {
  if (!canSaveDraftAction.value) return false;
  const projectSnapshotBeforeSave = captureProjectCompareFallbackBeforeSave();
  if (hasFormUnsavedChanges.value) {
    syncProjectInfoIntoSetupDraft();
    setupDirtySinceLastPersist.value = true;
  }
  const result = await persistSetupDraftWithRetry(false, { forceDraft: true });
  if (!result.serverSynced) return false;
  applyCapturedProjectCompareFallback(projectSnapshotBeforeSave);
  formBaselineSnapshot.value = serializeFormForCompare();
  return true;
};

const doPublishConfigNow = async (): Promise<boolean> => {
  if (!project.value || !canPublishAction.value) return false;
  try {
    if (hasFormUnsavedChanges.value) {
      syncProjectInfoIntoSetupDraft();
      setupDirtySinceLastPersist.value = true;
    }
    if (setupDirtySinceLastPersist.value || Boolean(autoSaveTimer.value)) {
      const draftSaved = await persistSetupDraftWithRetry(false, { forceDraft: true });
      if (!draftSaved.serverSynced) return false;
    }
    const { data } = await publishConfig(project.value.id, { expected_version: setupRevision.value });
    applySetupResponseMeta(data);
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    setupViewMode.value = "preview";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    const projectionStatus = String(data?.projection_status || "").toLowerCase();
    const projectionSummary = data?.projection_summary || {};
    const updatedCount = Number(projectionSummary.site_updated_count || 0);
    const skippedCount = Number(projectionSummary.site_skipped_count || 0);
    if (projectionStatus === "partial_success") {
      ElMessage.success(`配置已发布，已同步${updatedCount}个中心，跳过${skippedCount}个中心`);
    } else {
      ElMessage.success("配置已发布，已同步项目计划与中心入组目标");
    }
    const warnings = Array.isArray(projectionSummary.warnings) ? projectionSummary.warnings : [];
    if (warnings.length > 0) {
      await ElMessageBox.alert(warnings.slice(0, 3).join("\n"), "发布联动提示", {
        confirmButtonText: "我知道了",
      });
    }
    await refreshProjectDisplayAfterPublish();
    return true;
  } catch (e: any) {
    if (handleValidationFailure(e, "发布失败，请先修正配置")) {
      publishConfirmVisible.value = false;
      setupViewMode.value = "draft";
      return false;
    }
    if (e?.response?.status === 409) {
      ElMessage.warning("发布失败：配置已更新，正在刷新最新版本");
      await refreshSetupDraftFromServer({ silent: true });
      return false;
    }
    if (e?.response?.status === 403) {
      ElMessage.warning("项目已锁定，无法发布");
      if (project.value) project.value.is_locked = true;
      return false;
    }
    ElMessage.error(getApiErrorMessage(e, "发布失败"));
    return false;
  }
};

const publishConfigNow = async () => {
  if (!project.value || !canManageSetup.value) return;
  reconcileDraftSyncStateBeforePublish();
  if (hasSetupDraftUnsavedChanges.value) {
    ElMessage.warning("当前存在未上传的本地草稿，请先点击“保存配置”");
    return;
  }
  if (draftSyncStatus.value === "LOCAL_ONLY") {
    ElMessage.warning("当前草稿仅保存在本地，需先同步到服务端后才可发布");
    return;
  }
  if (canStrictlyDetectNoDiff.value && !hasPublishableDiff.value) {
    ElMessage.info("当前草稿与已发布快照一致，无需重复发布");
    return;
  }
  publishConfirmVisible.value = true;
};

const loadSetupVersionHistory = async () => {
  if (!project.value) return;
  rollbackDialogLoading.value = true;
  try {
    const { data } = await fetchSetupVersions(project.value.id);
    const history = (data || []).map((item) => ({
      ...item,
      published_at: formatDisplayTime(item.published_at),
    }));
    setupVersionHistory.value = history;
    const currentPublished = history.find((item) => item.is_current_published) || history[0];
    setupPublishedVersion.value = currentPublished
      ? (currentPublished.version_label || `v${currentPublished.display_version || currentPublished.version}`)
      : "v0";
    setupPublishedBranchName.value = currentPublished?.branch_name || "main";
    const activeDraftBase = history.find((item) => item.is_active_draft_base);
    if (activeDraftBase) {
      setupActiveDraftBaseVersion.value = activeDraftBase.version_label || `v${activeDraftBase.display_version || activeDraftBase.version}`;
    } else if (currentPublished && (currentPublished.branch_name || "main") === (setupCurrentBranchName.value || "main")) {
      setupActiveDraftBaseVersion.value = currentPublished.version_label || `v${currentPublished.display_version || currentPublished.version}`;
    } else {
      setupActiveDraftBaseVersion.value = "";
    }
  } catch (e: any) {
    ElMessage.error(getApiErrorMessage(e, "获取版本历史失败"));
  } finally {
    rollbackDialogLoading.value = false;
  }
};

const getDisplayVersionLabel = (rawVersion: number): string => {
  const found = setupVersionHistory.value.find((item) => item.version === rawVersion);
  if (!found) return `v${rawVersion}`;
  return found.version_label || `v${found.display_version || found.version}`;
};

const ROLLBACK_AXIS_LANE_GAP = 34;
const ROLLBACK_AXIS_LANE_START = 18;
const getRollbackAxisLeftStyle = (axisX?: number | null) => ({
  left: `${Number.isFinite(axisX) ? axisX : ROLLBACK_AXIS_LANE_START}px`,
});
const normalizeAxisBranch = (branchName?: string | null): string => (branchName || "main").trim() || "main";
const formatAxisLaneLabel = (branchName: string): string =>
  branchName === "main" ? "main" : branchName.replace(/^release\//, "");
const rollbackAxisLanes = computed<RollbackAxisLane[]>(() => {
  const laneMap = new Map<string, number>();
  laneMap.set("main", 0);
  setupVersionHistory.value.forEach((item) => {
    const branch = normalizeAxisBranch(item.branch_name);
    if (!laneMap.has(branch)) {
      laneMap.set(branch, laneMap.size);
    }
  });
  return Array.from(laneMap.entries()).map(([branch, index]) => ({
    branch,
    index,
    x: ROLLBACK_AXIS_LANE_START + index * ROLLBACK_AXIS_LANE_GAP,
  }));
});
const rollbackAxisLaneMap = computed(() => {
  const map = new Map<string, RollbackAxisLane>();
  rollbackAxisLanes.value.forEach((lane) => map.set(lane.branch, lane));
  return map;
});
const rollbackAxisWidth = computed(() => {
  const laneCount = rollbackAxisLanes.value.length;
  return Math.max(110, ROLLBACK_AXIS_LANE_START * 2 + Math.max(0, laneCount - 1) * ROLLBACK_AXIS_LANE_GAP + 12);
});
const resolveSpawnParentVersionId = (
  item: StudySetupConfigVersionItem,
  idMap: Map<string, StudySetupConfigVersionItem>
): string | null => {
  const itemBranch = normalizeAxisBranch(item.branch_name);
  if (itemBranch === "main") return null;
  const parentFromId = String(item.parent_version_id || "").trim();
  if (parentFromId) {
    const parent = idMap.get(parentFromId);
    if (parent && normalizeAxisBranch(parent.branch_name) !== itemBranch) {
      return parent.id;
    }
  }
  if (Number(item.branch_seq || 0) <= 1) {
    const fallbackMainParent = setupVersionHistory.value.find(
      (candidate) =>
        normalizeAxisBranch(candidate.branch_name) === "main" &&
        Number(candidate.display_version || 0) === Number(item.display_version || 0)
    );
    return fallbackMainParent?.id || null;
  }
  return null;
};
const rollbackAxisRows = computed<RollbackAxisRow[]>(() => {
  const idMap = new Map<string, StudySetupConfigVersionItem>();
  setupVersionHistory.value.forEach((item) => {
    idMap.set(item.id, item);
  });
  return setupVersionHistory.value.map((item) => {
    const branch = normalizeAxisBranch(item.branch_name);
    const lane = rollbackAxisLaneMap.value.get(branch);
    const axisX = lane?.x ?? ROLLBACK_AXIS_LANE_START;
    const mergedFromId = String(item.merged_from_version_id || "").trim();
    const mergedFrom = mergedFromId ? idMap.get(mergedFromId) : undefined;
    return {
      ...item,
      displayLabel: item.version_label || `v${item.display_version || item.version}`,
      mergeFromId: mergedFrom?.id || null,
      axisX,
      mergeFromLabel: mergedFrom ? (mergedFrom.version_label || `v${mergedFrom.display_version || mergedFrom.version}`) : "",
      spawnParentId: resolveSpawnParentVersionId(item, idMap),
    };
  });
});
const rollbackAxisScrollRef = ref<HTMLElement | null>(null);
const rollbackAxisRowTopMap = ref<Record<string, number>>({});
const rollbackAxisOverlayHeight = ref(0);
const rollbackAxisRowElementMap = new Map<string, HTMLElement>();
const setRollbackAxisRowRef = (el: HTMLElement | null, rowId: string) => {
  if (el) {
    rollbackAxisRowElementMap.set(rowId, el);
  } else {
    rollbackAxisRowElementMap.delete(rowId);
  }
};
const syncRollbackAxisLayout = () => {
  const scrollEl = rollbackAxisScrollRef.value;
  if (!scrollEl) return;
  const topMap: Record<string, number> = {};
  rollbackAxisRows.value.forEach((row) => {
    const rowEl = rollbackAxisRowElementMap.get(row.id);
    if (rowEl) {
      topMap[row.id] = rowEl.offsetTop;
    }
  });
  rollbackAxisRowTopMap.value = topMap;
  rollbackAxisOverlayHeight.value = scrollEl.scrollHeight;
};
const rollbackAxisSpawnConnectors = computed(() => {
  const rowMap = new Map<string, RollbackAxisRow>();
  rollbackAxisRows.value.forEach((row) => rowMap.set(row.id, row));
  return rollbackAxisRows.value
    .map((row) => {
      const parentId = row.spawnParentId;
      if (!parentId) return null;
      const parent = rowMap.get(parentId);
      if (!parent) return null;
      const sourceY = (rollbackAxisRowTopMap.value[parent.id] ?? -1) + 28;
      const targetY = (rollbackAxisRowTopMap.value[row.id] ?? -1) + 28;
      if (sourceY <= 0 || targetY <= 0) return null;
      return {
        key: `${parent.id}-${row.id}`,
        x1: parent.axisX,
        y1: sourceY,
        x2: row.axisX,
        y2: targetY,
      };
    })
    .filter(
      (
        item
      ): item is {
        key: string;
        x1: number;
        y1: number;
        x2: number;
        y2: number;
      } => Boolean(item)
    );
});
const rollbackAxisMergeConnectors = computed(() => {
  const rowMap = new Map<string, RollbackAxisRow>();
  rollbackAxisRows.value.forEach((row) => rowMap.set(row.id, row));
  return rollbackAxisRows.value
    .map((row) => {
      const sourceId = row.mergeFromId;
      if (!sourceId) return null;
      const source = rowMap.get(sourceId);
      if (!source) return null;
      const sourceY = (rollbackAxisRowTopMap.value[source.id] ?? -1) + 28;
      const targetY = (rollbackAxisRowTopMap.value[row.id] ?? -1) + 28;
      if (sourceY <= 0 || targetY <= 0) return null;
      return {
        key: `${source.id}->${row.id}`,
        x1: source.axisX,
        y1: sourceY,
        x2: row.axisX,
        y2: targetY,
      };
    })
    .filter(
      (
        item
      ): item is {
        key: string;
        x1: number;
        y1: number;
        x2: number;
        y2: number;
      } => Boolean(item)
    );
});
/* 同分支连续版本间的连接器 - 显示 release 分支内的继承关系 */
const rollbackAxisSameBranchConnectors = computed(() => {
  const branchGroups = new Map<string, RollbackAxisRow[]>();
  rollbackAxisRows.value.forEach((row) => {
    const branch = normalizeAxisBranch(row.branch_name);
    if (!branchGroups.has(branch)) {
      branchGroups.set(branch, []);
    }
    branchGroups.get(branch)!.push(row);
  });
  const connectors: { key: string; x: number; y1: number; y2: number; isMain: boolean }[] = [];
  branchGroups.forEach((rows, branch) => {
    if (rows.length < 2) return;
    for (let i = 0; i < rows.length - 1; i++) {
      const topRow = rows[i];
      const bottomRow = rows[i + 1];
      const topY = (rollbackAxisRowTopMap.value[topRow.id] ?? -1) + 28;
      const bottomY = (rollbackAxisRowTopMap.value[bottomRow.id] ?? -1) + 28;
      if (topY <= 0 || bottomY <= 0) continue;
      connectors.push({
        key: `sb-${topRow.id}-${bottomRow.id}`,
        x: topRow.axisX,
        y1: topY,
        y2: bottomY,
        isMain: branch === "main",
      });
    }
  });
  return connectors;
});
watch(
  () => [rollbackDialogVisible.value, rollbackAxisRows.value.map((item) => item.id).join("|"), rollbackAxisWidth.value],
  async ([visible]) => {
    if (!visible) return;
    await nextTick();
    syncRollbackAxisLayout();
  }
);
onMounted(() => {
  window.addEventListener("resize", syncRollbackAxisLayout);
});

const isLatestPublishedVersion = (rawVersion: number): boolean => {
  const currentPublished = setupVersionHistory.value.find((item) => item.is_current_published) || setupVersionHistory.value[0];
  return Boolean(currentPublished && currentPublished.version === rawVersion);
};

const openRollbackDialog = async () => {
  if (!project.value || !canManageSetup.value) return;
  await loadSetupVersionHistory();
  rollbackDialogVisible.value = true;
};

const selectRollbackVersion = async (targetVersion: number) => {
  if (!project.value) return;
  const targetDisplayVersion = getDisplayVersionLabel(targetVersion);
  try {
    await ElMessageBox.confirm(
      `将立即回滚并替换当前发布版本为 ${targetDisplayVersion}，同时将草稿切换到对应分支继续开发，是否继续？`,
      "回滚确认",
      {
        type: "warning",
        confirmButtonText: "确认回滚",
        cancelButtonText: "取消",
      }
    );
  } catch {
    return;
  }
  rollbackDialogLoading.value = true;
  try {
    const { data } = await rollbackConfig(project.value.id, {
      expected_version: setupRevision.value,
      target_version: targetVersion,
    });
    applySetupResponseMeta(data);
    applySetupDraft(clone(data.data));
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    setupViewMode.value = "draft";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    await loadSetupVersionHistory();
    rollbackDialogVisible.value = false;
    const currentBranch = setupCurrentBranchName.value || "main";
    ElMessage.success(`已回滚并替换当前发布为 ${setupPublishedVersionText.value}，当前版本分支 ${currentBranch}`);
  } catch (e: any) {
    if (e?.response?.status === 409) {
      ElMessage.warning("回滚失败：版本冲突，正在刷新最新配置");
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      return;
    }
    ElMessage.error(getApiErrorMessage(e, "版本回滚失败"));
  } finally {
    rollbackDialogLoading.value = false;
  }
};

const checkoutBranchDraftFromVersion = async (targetVersion: number) => {
  if (!project.value) return;
  const targetDisplayVersion = getDisplayVersionLabel(targetVersion);
  try {
    await ElMessageBox.confirm(
      `将基于 ${targetDisplayVersion} 创建/切换分支草稿，不会改变当前发布版本，是否继续？`,
      "新建分支草稿确认",
      {
        type: "warning",
        confirmButtonText: "确认切换",
        cancelButtonText: "取消",
      }
    );
  } catch {
    return;
  }
  rollbackDialogLoading.value = true;
  try {
    const { data } = await checkoutSetupBranchDraft(project.value.id, {
      expected_version: setupRevision.value,
      target_version: targetVersion,
    });
    applySetupResponseMeta(data);
    applySetupDraft(clone(data.data));
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    setupViewMode.value = "draft";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    const checkoutRevision = data.version;

    try {
      const { data: refillData } = await refillSetupDraftOnServer(project.value.id, {
        expected_version: checkoutRevision,
      });
      applySetupResponseMeta(refillData);
      applySetupDraft(clone(refillData.data));
      clearSetupValidationErrors();
      markSetupSynced();
      clearLocalSetupDraft();
      setupViewMode.value = "draft";
      setupSaveMeta.value = {
        updatedAt: formatDisplayTime(refillData.updated_at),
        savedBy: refillData.saved_by_name || resolveSavedByDisplay(refillData.saved_by),
        serverSynced: true,
      };
      updateLastServerSaveMeta(refillData.updated_at, refillData.saved_by_name || resolveSavedByDisplay(refillData.saved_by));
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      const currentBranch = setupCurrentBranchName.value || "main";
      ElMessage.success(`已切换到分支草稿 ${currentBranch}，并自动回填为 ${targetDisplayVersion}`);
    } catch (refillError: any) {
      if (refillError?.response?.status === 409) {
        ElMessage.warning("分支已切换，但自动回填发生版本冲突，正在刷新最新配置");
      } else {
        ElMessage.warning(getApiErrorMessage(refillError, "分支已切换，但自动回填失败"));
      }
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
    }
  } catch (e: any) {
    if (e?.response?.status === 409) {
      ElMessage.warning("切换失败：版本冲突，正在刷新最新配置");
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      return;
    }
    ElMessage.error(getApiErrorMessage(e, "新建分支草稿失败"));
  } finally {
    rollbackDialogLoading.value = false;
  }
};

const excelEscapeXml = (value: string): string =>
  value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");

const normalizeExcelCellValue = (value: unknown): string | number => {
  if (value === null || value === undefined) return "";
  if (typeof value === "number") return Number.isFinite(value) ? value : "";
  if (typeof value === "boolean") return value ? "是" : "否";
  if (typeof value === "string") return value;
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
};

const buildExcelCellXml = (value: unknown, styleId = "cell"): string => {
  const normalized = normalizeExcelCellValue(value);
  if (typeof normalized === "number") {
    return `<Cell ss:StyleID="${styleId}"><Data ss:Type="Number">${normalized}</Data></Cell>`;
  }
  return `<Cell ss:StyleID="${styleId}"><Data ss:Type="String">${excelEscapeXml(normalized)}</Data></Cell>`;
};

const sanitizeExcelSheetName = (name: string): string => {
  const replaced = String(name || "")
    .replace(/[\\/:*?\[\]]/g, "_")
    .trim();
  if (!replaced) return "Sheet";
  return replaced.slice(0, 31);
};

const formatEnrollmentTargetsText = (raw: Record<string, number>): string => {
  const entries = Object.entries(raw || {})
    .filter(([key, value]) => key && value !== null && value !== undefined)
    .sort(([a], [b]) => a.localeCompare(b, "zh-CN"));
  if (!entries.length) return "-";
  return entries.map(([key, value]) => `${key}:${normalizeEnrollmentTarget(value)}`).join("；");
};

const buildExcelWorksheetXml = (
  name: string,
  metaRows: Array<[string, unknown]>,
  headers: string[],
  rows: unknown[][]
): string => {
  const sheetName = sanitizeExcelSheetName(name);
  const columnCount = Math.max(1, headers.length);
  const safeRows = rows.length > 0 ? rows : [[...Array(columnCount - 1).fill(""), "无数据"]];
  const metaXml = metaRows
    .map(
      ([key, value]) =>
        `<Row>${buildExcelCellXml(key, "metaKey")}<Cell ss:StyleID="metaValue" ss:MergeAcross="${Math.max(columnCount - 2, 0)}"><Data ss:Type="String">${excelEscapeXml(
          String(normalizeExcelCellValue(value) || "-")
        )}</Data></Cell></Row>`
    )
    .join("");
  const headerXml = `<Row>${headers.map((header) => buildExcelCellXml(header, "header")).join("")}</Row>`;
  const dataXml = safeRows
    .map((row) => `<Row>${headers.map((_, idx) => buildExcelCellXml(row[idx] ?? "", "cell")).join("")}</Row>`)
    .join("");
  return `<Worksheet ss:Name="${excelEscapeXml(sheetName)}"><Table>${metaXml}<Row/>${headerXml}${dataXml}</Table></Worksheet>`;
};

const downloadVersionRaw = (versionItem: StudySetupConfigVersionItem) => {
  if (!project.value) return;
  const displayVersion = getDisplayVersionLabel(versionItem.version);
  const branchName = versionItem.branch_name || "main";
  const configDraft = isSetupDraftShape(versionItem.config) ? versionItem.config : createDefaultSetupDraft([]);
  const projectSnapshot = isProjectPublishSnapshotShape(versionItem.published_project_snapshot)
    ? versionItem.published_project_snapshot
    : null;
  const metaRows: Array<[string, unknown]> = [
    ["导出版本", displayVersion],
    ["版本分支", branchName],
    ["发布人", versionItem.published_by_name || "-"],
    ["发布时间", versionItem.published_at || "-"],
  ];

  const step1Headers = ["字段", "值"];
  const step1Rows: unknown[][] = [
    ["项目名称", projectSnapshot?.name || "-"],
    ["项目全称", projectSnapshot?.project_full_name || "-"],
    ["申办方", projectSnapshot?.sponsor || "-"],
    ["项目编号", projectSnapshot?.code || "-"],
    ["方案号", projectSnapshot?.protocol_no || "-"],
    ["组长单位", projectSnapshot?.lead_unit || "-"],
    ["主要研究者", projectSnapshot?.principal_investigator || "-"],
    ["主PM", projectSnapshot?.main_pm || "-"],
    ["研究分期", projectSnapshot?.research_analysis || "-"],
    ["研究产品", projectSnapshot?.research_product || "-"],
    ["对照产品", projectSnapshot?.control_product || "-"],
    ["适应症", projectSnapshot?.indication || "-"],
    ["研究人群", projectSnapshot?.research_population || "-"],
    ["研究设计", projectSnapshot?.research_design || "-"],
    ["计划开始日期", projectSnapshot?.plan_start_date || "-"],
    ["计划结束日期", projectSnapshot?.plan_end_date || "-"],
    ["项目状态", projectSnapshot?.status ? statusLabel(projectSnapshot.status) : "-"],
    ["计划中心数", projectSnapshot?.planned_site_count ?? "-"],
    ["计划入组数", projectSnapshot?.planned_enrollment_count ?? "-"],
    ["访视计划", formatVisitSchedule(projectSnapshot?.visit_schedule)],
  ];

  const step2Headers = ["里程碑", "计划日期", "开始日期", "结束日期", "耗时(天)", "负责人", "状态", "备注"];
  const step2Rows = configDraft.projectMilestones.map((item) => [
    item.name || "-",
    item.planDate || "-",
    item.startDate || "-",
    item.endDate || "-",
    item.durationDays ?? "-",
    item.owner || "-",
    item.status || "-",
    item.remark || "-",
  ]);

  const step3Headers = ["计划开始日期", "计划结束日期", "当前周期", "计划入组总数", "周期类型", "周期", "计划人数", "备注"];
  const parsedPlan = parseEnrollmentStageBreakdown(configDraft.enrollmentPlan.stageBreakdown);
  const step3CycleRows: unknown[][] = [];
  (["month", "quarter"] as EnrollmentCycle[]).forEach((cycle) => {
    const entries = Object.entries(parsedPlan.targetsByCycle[cycle] || {})
      .filter(([key, value]) => key && value !== null && value !== undefined)
      .sort(([a], [b]) => a.localeCompare(b, "zh-CN"));
    entries.forEach(([key, value]) => {
      step3CycleRows.push([
        configDraft.enrollmentPlan.startDate || "-",
        configDraft.enrollmentPlan.endDate || "-",
        enrollmentCycleLabelMap[parsedPlan.cycle] || parsedPlan.cycle,
        normalizeEnrollmentTarget(configDraft.enrollmentPlan.totalTarget),
        enrollmentCycleLabelMap[cycle] || cycle,
        key,
        normalizeEnrollmentTarget(value),
        configDraft.enrollmentPlan.monthlyGoalNote || "-",
      ]);
    });
  });
  if (!step3CycleRows.length) {
    step3CycleRows.push([
      configDraft.enrollmentPlan.startDate || "-",
      configDraft.enrollmentPlan.endDate || "-",
      enrollmentCycleLabelMap[parsedPlan.cycle] || parsedPlan.cycle,
      normalizeEnrollmentTarget(configDraft.enrollmentPlan.totalTarget),
      "-",
      "-",
      "-",
      configDraft.enrollmentPlan.monthlyGoalNote || "-",
    ]);
  }

  const step4Headers = ["中心ID", "中心名称", "里程碑", "计划日期", "负责人", "状态", "备注"];
  const step4Rows = configDraft.siteMilestones.map((item) => [
    item.id || "-",
    item.id ? getSiteName(item.id) : "-",
    item.milestone || "-",
    item.planDate || "-",
    item.owner || "-",
    item.status || "-",
    item.remark || "-",
  ]);

  const step5Headers = ["中心ID", "中心名称", "计划入组数", "开始日期", "结束日期", "月度分解", "季度分解", "备注"];
  const step5Rows = configDraft.siteEnrollmentPlans.map((item) => {
    const parsed = parseSiteEnrollmentStageBreakdown(item.stageBreakdown);
    return [
      item.siteId || "-",
      item.siteName || "-",
      normalizeEnrollmentTarget(item.target),
      item.startDate || "-",
      item.endDate || "-",
      formatEnrollmentTargetsText(parsed.month),
      formatEnrollmentTargetsText(parsed.quarter),
      item.note || "-",
    ];
  });

  const step6Headers = ["中心ID", "中心名称", "确认人", "确认状态", "确认日期", "备注"];
  const step6Rows = configDraft.centerConfirm.map((item) => [
    item.siteId || "-",
    item.siteName || "-",
    item.confirmer || "-",
    item.confirmStatus || "-",
    item.confirmDate || "-",
    item.note || "-",
  ]);

  const workbookXml = `<?xml version="1.0" encoding="UTF-8"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:html="http://www.w3.org/TR/REC-html40">
<Styles>
  <Style ss:ID="Default" ss:Name="Normal">
    <Alignment ss:Vertical="Center"/>
    <Font ss:FontName="Microsoft YaHei" ss:Size="10"/>
  </Style>
  <Style ss:ID="header">
    <Font ss:FontName="Microsoft YaHei" ss:Size="10" ss:Bold="1"/>
    <Interior ss:Color="#EAF1FB" ss:Pattern="Solid"/>
    <Borders>
      <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#C8D6EA"/>
      <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#C8D6EA"/>
      <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#C8D6EA"/>
      <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#C8D6EA"/>
    </Borders>
  </Style>
  <Style ss:ID="cell">
    <Borders>
      <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#E1E8F3"/>
      <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#E1E8F3"/>
      <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#E1E8F3"/>
      <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1" ss:Color="#E1E8F3"/>
    </Borders>
  </Style>
  <Style ss:ID="metaKey">
    <Font ss:FontName="Microsoft YaHei" ss:Size="10" ss:Bold="1"/>
    <Interior ss:Color="#F4F7FC" ss:Pattern="Solid"/>
  </Style>
  <Style ss:ID="metaValue">
    <Alignment ss:Vertical="Center" ss:WrapText="1"/>
    <Interior ss:Color="#FCFDFF" ss:Pattern="Solid"/>
  </Style>
</Styles>
${buildExcelWorksheetXml("第1步-项目信息", metaRows, step1Headers, step1Rows)}
${buildExcelWorksheetXml("第2步-项目里程碑", metaRows, step2Headers, step2Rows)}
${buildExcelWorksheetXml("第3步-项目入组计划", metaRows, step3Headers, step3CycleRows)}
${buildExcelWorksheetXml("第4步-中心里程碑", metaRows, step4Headers, step4Rows)}
${buildExcelWorksheetXml("第5步-中心入组计划", metaRows, step5Headers, step5Rows)}
${buildExcelWorksheetXml("第6步-中心确认", metaRows, step6Headers, step6Rows)}
</Workbook>`;

  const blob = new Blob([`\uFEFF${workbookXml}`], { type: "application/vnd.ms-excel;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const safeVersionLabel = displayVersion.replace(/[^\w.-]/g, "_");
  const filename = `setup-config-${safeVersionLabel}-${project.value.code || project.value.id}.xls`;
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
};

const removeVersion = async (targetVersion: number) => {
  if (!project.value) return;
  if (isLatestPublishedVersion(targetVersion)) {
    ElMessage.warning("当前发布版本不能删除");
    return;
  }
  const displayVersion = getDisplayVersionLabel(targetVersion);
  try {
    await ElMessageBox.confirm(`确认删除发布版本 ${displayVersion} 的快照？此操作不可恢复。`, "删除确认", {
      type: "warning",
      confirmButtonText: "确认删除",
      cancelButtonText: "取消",
    });
  } catch {
    return;
  }
  rollbackDialogLoading.value = true;
  try {
    await deleteSetupVersion(project.value.id, targetVersion);
    await loadSetupVersionHistory();
    ElMessage.success(`已删除发布版本 ${displayVersion}`);
  } catch (e: any) {
    ElMessage.error(getApiErrorMessage(e, "删除版本失败"));
  } finally {
    rollbackDialogLoading.value = false;
  }
};

const mergeVersionToMain = async (sourceVersion: number) => {
  if (!project.value) return;
  const sourceVersionLabel = getDisplayVersionLabel(sourceVersion);
  try {
    await ElMessageBox.confirm(
      `确认将 ${sourceVersionLabel} 合并到主分支并生成新的主版本号？`,
      "合并主分支确认",
      {
        type: "warning",
        confirmButtonText: "确认合并",
        cancelButtonText: "取消",
      }
    );
  } catch {
    return;
  }
  rollbackDialogLoading.value = true;
  try {
    const { data } = await mergeSetupVersionToMain(project.value.id, {
      expected_version: setupRevision.value,
      source_version: sourceVersion,
    });
    applySetupResponseMeta(data);
    applySetupDraft(clone(data.data));
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    setupViewMode.value = "preview";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    await loadSetupVersionHistory();
    ElMessage.success(`${sourceVersionLabel} 已合并到主分支，当前发布为 ${setupPublishedVersionText.value}`);
  } catch (e: any) {
    if (e?.response?.status === 409) {
      ElMessage.warning("合并失败：版本冲突，正在刷新最新配置");
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      return;
    }
    ElMessage.error(getApiErrorMessage(e, "合并主分支失败"));
  } finally {
    rollbackDialogLoading.value = false;
  }
};

const handleRefillDraft = async () => {
  if (!project.value || !canManageSetup.value) return;
  try {
    await ElMessageBox.confirm(
      `确认将草稿一键回填为当前发布版本 ${setupPublishedVersionText.value} 的内容？`,
      "一键回填确认",
      {
        type: "warning",
        confirmButtonText: "确认回填",
        cancelButtonText: "取消",
      }
    );
  } catch {
    return;
  }
  primarySaveLoading.value = true;
  try {
    const { data } = await refillSetupDraftOnServer(project.value.id, {
      expected_version: setupRevision.value,
    });
    applySetupResponseMeta(data);
    applySetupDraft(clone(data.data));
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    setupViewMode.value = "draft";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    await refreshSetupDraftFromServer({ silent: true });
    await loadSetupVersionHistory();
    ElMessage.success(`草稿已回填为 ${setupPublishedVersionText.value}`);
  } catch (e: any) {
    if (e?.response?.status === 409) {
      ElMessage.warning("回填失败：版本冲突，正在刷新最新配置");
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      return;
    }
    ElMessage.error(getApiErrorMessage(e, "草稿回填失败"));
  } finally {
    primarySaveLoading.value = false;
  }
};

const handleClearDraft = async () => {
  if (!project.value || !canManageSetup.value) return;
  try {
    await ElMessageBox.confirm(
      "确认清空当前立项配置草稿的所有数据？该操作不会影响已发布版本。",
      "清空草稿确认",
      {
        type: "warning",
        confirmButtonText: "确认清空",
        cancelButtonText: "取消",
      }
    );
  } catch {
    return;
  }
  primarySaveLoading.value = true;
  try {
    const { data } = await clearSetupDraftOnServer(project.value.id, {
      expected_version: setupRevision.value,
    });
    applySetupResponseMeta(data);
    applySetupDraft(clone(data.data));
    clearSetupValidationErrors();
    markSetupSynced();
    clearLocalSetupDraft();
    clearLocalProjectDraft();
    setupViewMode.value = "draft";
    setupSaveMeta.value = {
      updatedAt: formatDisplayTime(data.updated_at),
      savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
      serverSynced: true,
    };
    updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
    await refreshSetupDraftFromServer({ silent: true });
    await loadSetupVersionHistory();
    ElMessage.success(`草稿已清空（当前发布仍为 ${setupPublishedVersionText.value}）`);
  } catch (e: any) {
    if (e?.response?.status === 409) {
      ElMessage.warning("清空失败：版本冲突，正在刷新最新配置");
      await refreshSetupDraftFromServer({ silent: true });
      await loadSetupVersionHistory();
      return;
    }
    ElMessage.error(getApiErrorMessage(e, "清空草稿失败"));
  } finally {
    primarySaveLoading.value = false;
  }
};

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 (!ensureCanSaveDraftAction()) return;
  if (incompleteSteps.value.length > 0) {
    try {
      await ElMessageBox.confirm(
        `当前仍有 ${incompleteSteps.value.length} 个步骤未完成，确认后将继续保存草稿。`,
        "步骤未完成",
        {
          type: "warning",
          confirmButtonText: "确认保存",
          cancelButtonText: "继续编辑",
        }
      );
    } catch {
      return;
    }
  }
  primarySaveLoading.value = true;
  try {
    const saved = await saveConfigNow();
    if (!saved) return;
    closeEditDrawerAfterSave();
    ElMessage.success("配置草稿已保存");
  } finally {
    primarySaveLoading.value = false;
  }
};

const confirmPublishConfig = async () => {
  publishConfirmLoading.value = true;
  try {
    const success = await doPublishConfigNow();
    if (success) {
      publishConfirmVisible.value = false;
      await loadSetupVersionHistory();
    }
  } finally {
    publishConfirmLoading.value = false;
  }
};

const enterProject = async () => {
  if (!project.value) return;
  studyStore.setCurrentStudy(project.value);
  await studyStore.loadCurrentStudyPermissions().catch(() => {});
  router.push("/project/overview");
};
const goMembers = () => {
  if (!project.value) return;
  router.push(`/admin/projects/${project.value.id}/members`);
};
const goSites = () => {
  if (!project.value) return;
  router.push(`/admin/projects/${project.value.id}/sites`);
};
const handleSecondaryAction = (command: string) => {
  if (command === "enter") {
    enterProject();
    return;
  }
  if (command === "members") {
    goMembers();
    return;
  }
  if (command === "sites") {
    goSites();
    return;
  }
};

const prevStep = () => {
  if (isEditing.value) return;
  if (activeStep.value <= 0) return;
  activeStep.value -= 1;
};
const nextStep = () => {
  if (isEditing.value) return;
  if (activeStep.value >= steps.length - 1) return;
  activeStep.value += 1;
};
const goStep = (index: number) => {
  if (isEditing.value) return;
  activeStep.value = index;
};

const addProjectMilestone = () => {
  if (!canMutateDraft()) return;
  setupDraft.projectMilestones.push({
    id: makeId(),
    name: "",
    planDate: "",
    startDate: "",
    endDate: "",
    durationDays: 1,
    owner: "",
    remark: "",
    status: "未开始",
  });
};
const ensureStep2Editable = (): boolean => {
  if (!project.value) return false;
  if (isPublishedView.value) {
    ElMessage.info("当前为只读视图，请先切换到草稿后再操作");
    return false;
  }
  if (!canManageSetup.value) {
    ElMessage.warning("当前角色无权编辑立项配置");
    return false;
  }
  if (project.value.is_locked) {
    ElMessage.warning("项目已锁定，无法编辑。");
    return false;
  }
  return true;
};
const handleAddProjectMilestoneAction = () => {
  if (!ensureStep2Editable()) return;
  addProjectMilestone();
};
const onProjectMilestoneTemplateSelect = async (templateKey: string) => {
  if (!ensureStep2Editable()) return;
  const template = projectMilestoneTemplates.find((item) => item.key === templateKey);
  if (!template) return;

  if (setupDraft.projectMilestones.length > 0) {
    try {
      await ElMessageBox.confirm("应用模板将覆盖当前第2步里程碑列表，是否继续？", "应用模板", {
        type: "warning",
        confirmButtonText: "覆盖并应用",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }

  setupDraft.projectMilestones = template.milestones.map((name) => ({
    id: makeId(),
    name,
    planDate: "",
    startDate: "",
    endDate: "",
    durationDays: 1,
    owner: "",
    remark: "",
    status: "未开始",
  }));
  ElMessage.success(`已应用模板：${template.label}`);
};
const removeProjectMilestone = (index: number) => {
  if (!canMutateDraft()) return;
  setupDraft.projectMilestones.splice(index, 1);
};
const openProjectMilestoneEditor = (index: number) => {
  openIndexedEditor(setupDraft.projectMilestones, index, projectMilestoneEditor, (row) => {
    projectMilestoneEditorForm.id = row.id || "";
    projectMilestoneEditorForm.name = row.name || "";
    projectMilestoneEditorForm.startDate = resolveProjectMilestoneStart(row);
    projectMilestoneEditorForm.endDate = resolveProjectMilestoneEnd(row);
    projectMilestoneEditorForm.durationDays = resolveProjectMilestoneDurationDays(row) || 1;
    projectMilestoneEditorForm.planDate = resolveProjectMilestoneStart(row);
    projectMilestoneEditorForm.owner = row.owner || "";
    projectMilestoneEditorForm.remark = row.remark || "";
    projectMilestoneEditorForm.status = row.status || "未开始";
  });
};
const saveProjectMilestoneEditor = () => {
  if (!canMutateDraft()) return;
  const index = getEditingIndex(projectMilestoneEditor, setupDraft.projectMilestones.length);
  if (index < 0) return;
  const name = (projectMilestoneEditorForm.name || "").trim();
  if (!name) {
    ElMessage.warning("请填写里程碑名称");
    return;
  }
  const startDate = normalizePlanDate(projectMilestoneEditorForm.startDate || projectMilestoneEditorForm.planDate);
  const endDate = normalizePlanDate(projectMilestoneEditorForm.endDate || projectMilestoneEditorForm.planDate);
  if (!startDate) {
    ElMessage.warning("请选择开始日期");
    return;
  }
  if (!endDate) {
    ElMessage.warning("请选择结束日期");
    return;
  }
  if (startDate > endDate) {
    ElMessage.warning("结束日期不能早于开始日期");
    return;
  }
  const durationDays = calculateDurationDays(startDate, endDate);
  setupDraft.projectMilestones[index] = {
    id: projectMilestoneEditorForm.id || makeId(),
    name,
    planDate: startDate,
    startDate,
    endDate,
    durationDays: durationDays > 0 ? durationDays : 1,
    owner: (projectMilestoneEditorForm.owner || "").trim(),
    remark: (projectMilestoneEditorForm.remark || "").trim(),
    status: projectMilestoneEditorForm.status || "未开始",
  };
  closeIndexedEditor(projectMilestoneEditor, "里程碑已更新");
};

const addSiteMilestone = () => {
  if (!canMutateDraft()) return;
  setupDraft.siteMilestones.push({ id: makeId(), milestone: "", planDate: "", owner: "", remark: "", status: "未开始" });
};
const handleAddSiteMilestoneAction = () => {
  if (!ensureStep2Editable()) return;
  addSiteMilestone();
};
const onSiteMilestoneTemplateSelect = async (templateKey: string) => {
  if (!ensureStep2Editable()) return;
  const template = siteMilestoneTemplates.find((item) => item.key === templateKey);
  if (!template) return;

  if (setupDraft.siteMilestones.length > 0) {
    try {
      await ElMessageBox.confirm("应用模板将覆盖当前第4步中心里程碑列表，是否继续？", "应用模板", {
        type: "warning",
        confirmButtonText: "覆盖并应用",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }

  setupDraft.siteMilestones = template.milestones.map((milestone) => ({
    id: makeId(),
    milestone,
    planDate: "",
    owner: "",
    remark: "",
    status: "未开始",
  }));
  ElMessage.success(`已应用模板：${template.label}`);
};
const removeSiteMilestone = (index: number) => {
  if (!canMutateDraft()) return;
  setupDraft.siteMilestones.splice(index, 1);
};
const openSiteMilestoneEditor = (index: number) => {
  openIndexedEditor(setupDraft.siteMilestones, index, siteMilestoneEditor, (row) => {
    siteMilestoneEditorForm.id = row.id || "";
    siteMilestoneEditorForm.milestone = row.milestone || "";
    siteMilestoneEditorForm.planDate = row.planDate || "";
    siteMilestoneEditorForm.owner = row.owner || "";
    siteMilestoneEditorForm.remark = row.remark || "";
    siteMilestoneEditorForm.status = row.status || "未开始";
  });
};
const saveSiteMilestoneEditor = () => {
  if (!canMutateDraft()) return;
  const index = getEditingIndex(siteMilestoneEditor, setupDraft.siteMilestones.length);
  if (index < 0) return;
  const milestone = (siteMilestoneEditorForm.milestone || "").trim();
  if (!milestone) {
    ElMessage.warning("请填写里程碑");
    return;
  }
  if (!siteMilestoneEditorForm.planDate) {
    ElMessage.warning("请选择计划日期");
    return;
  }
  const { start: planStart, end: planEnd } = getProjectPlanWindow();
  const planDate = normalizePlanDate(siteMilestoneEditorForm.planDate);
  if (planStart && planDate < planStart) {
    ElMessage.warning("中心里程碑日期不能早于项目计划开始日期");
    return;
  }
  if (planEnd && planDate > planEnd) {
    ElMessage.warning("中心里程碑日期不能晚于项目计划结束日期");
    return;
  }
  setupDraft.siteMilestones[index] = {
    id: siteMilestoneEditorForm.id || makeId(),
    milestone,
    planDate: siteMilestoneEditorForm.planDate,
    owner: (siteMilestoneEditorForm.owner || "").trim(),
    remark: (siteMilestoneEditorForm.remark || "").trim(),
    status: siteMilestoneEditorForm.status || "未开始",
  };
  closeIndexedEditor(siteMilestoneEditor, "中心里程碑已更新");
};

const removeSiteEnrollment = (index: number) => {
  if (!canMutateDraft()) return;
  setupDraft.siteEnrollmentPlans.splice(index, 1);
};
const openSiteEnrollmentEditor = (index: number) => {
  openIndexedEditor(setupDraft.siteEnrollmentPlans, index, siteEnrollmentEditor, (row) => {
    siteEnrollmentEditorForm.id = row.id || "";
    siteEnrollmentEditorForm.siteId = row.siteId || "";
    siteEnrollmentEditorForm.siteName = row.siteName || "";
    siteEnrollmentEditorForm.target = row.target ?? 0;
    siteEnrollmentEditorForm.startDate = row.startDate || "";
    siteEnrollmentEditorForm.endDate = row.endDate || "";
    siteEnrollmentEditorForm.note = row.note || "";
    siteEnrollmentEditorForm.stageBreakdown = row.stageBreakdown || "";
  });
};
const saveSiteEnrollmentEditor = () => {
  if (!canMutateDraft()) return;
  const index = getEditingIndex(siteEnrollmentEditor, setupDraft.siteEnrollmentPlans.length);
  if (index < 0) return;
  if (!siteEnrollmentEditorForm.siteId) {
    ElMessage.warning("请选择中心");
    return;
  }
  if (isSiteEnrollmentDuplicate(siteEnrollmentEditorForm.siteId, index)) {
    ElMessage.warning("该中心已配置入组计划，请勿重复配置");
    return;
  }
  if (!siteEnrollmentEditorForm.startDate || !siteEnrollmentEditorForm.endDate) {
    ElMessage.warning("请填写启动日期和完成日期");
    return;
  }
  if (siteEnrollmentEditorForm.startDate > siteEnrollmentEditorForm.endDate) {
    ElMessage.warning("完成日期不能早于启动日期");
    return;
  }
  const { start: planStart, end: planEnd } = getProjectPlanWindow();
  const startDate = normalizePlanDate(siteEnrollmentEditorForm.startDate);
  const endDate = normalizePlanDate(siteEnrollmentEditorForm.endDate);
  if (planStart && startDate < planStart) {
    ElMessage.warning("中心启动日期不能早于项目计划开始日期");
    return;
  }
  if (planEnd && startDate > planEnd) {
    ElMessage.warning("中心启动日期不能晚于项目计划结束日期");
    return;
  }
  if (planStart && endDate < planStart) {
    ElMessage.warning("中心完成日期不能早于项目计划开始日期");
    return;
  }
  if (planEnd && endDate > planEnd) {
    ElMessage.warning("中心完成日期不能晚于项目计划结束日期");
    return;
  }
  setupDraft.siteEnrollmentPlans[index] = {
    id: siteEnrollmentEditorForm.id || makeId(),
    siteId: siteEnrollmentEditorForm.siteId,
    siteName: getSiteName(siteEnrollmentEditorForm.siteId),
    target: Math.max(0, Number(siteEnrollmentEditorForm.target || 0)),
    startDate: siteEnrollmentEditorForm.startDate,
    endDate: siteEnrollmentEditorForm.endDate,
    note: (siteEnrollmentEditorForm.note || "").trim(),
    stageBreakdown: siteEnrollmentEditorForm.stageBreakdown || "",
  };
  closeIndexedEditor(siteEnrollmentEditor, "中心入组计划已更新");
};

const addCenterConfirm = () => {
  if (!canMutateDraft()) return;
  setupDraft.centerConfirm.push({ id: makeId(), siteId: "", siteName: "", confirmer: "", confirmStatus: "待确认", confirmDate: "", note: "" });
};
const removeCenterConfirm = (index: number) => {
  if (!canMutateDraft()) return;
  setupDraft.centerConfirm.splice(index, 1);
};

const handleBeforeUnload = (event: BeforeUnloadEvent) => {
  if (!hasLeaveGuardChanges.value) return;
  event.preventDefault();
  event.returnValue = "";
};

onBeforeRouteLeave(async () => {
  if (!hasLeaveGuardChanges.value) return true;
  try {
    await ElMessageBox.confirm("当前有未保存或未同步到服务端的修改，确定离开当前页面？", "离开确认", {
      type: "warning",
      confirmButtonText: "确定离开",
      cancelButtonText: "继续编辑",
    });
    return true;
  } catch {
    return false;
  }
});

onMounted(() => {
  void loadProject();
  window.addEventListener("beforeunload", handleBeforeUnload);
});
onBeforeUnmount(() => {
  window.removeEventListener("resize", syncRollbackAxisLayout);
  window.removeEventListener("beforeunload", handleBeforeUnload);
  if (autoSaveTimer.value) {
    window.clearTimeout(autoSaveTimer.value);
  }
  if (drawerCloseTimer.value) {
    window.clearTimeout(drawerCloseTimer.value);
  }
});
</script>

<style scoped>
.setup-page {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: #f3f6fb;
}

.setup-shell {
  border: 0;
  background: #ffffff;
  overflow: hidden;
  box-shadow: none;
}

.setup-header {
  position: relative;
  z-index: 1250;
  background: linear-gradient(135deg, #0b1426 0%, #17346b 50%, #2f69d2 100%);
  padding: 6px 16px; /* 极限垂直留白压缩 */
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}

.setup-flow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px; /* 极限缩短行间距 */
  gap: 0;
}

.flow-step {
  display: flex;
  align-items: center;
  flex: 1 1 0;
  min-width: 0;
  white-space: nowrap;
  cursor: pointer;
  color: rgba(255, 255, 255, 0.55);
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.flow-step:hover {
  color: rgba(255, 255, 255, 0.85);
}

.flow-step.active {
  color: #ffffff;
  text-shadow: 0 0 12px rgba(255, 255, 255, 0.4);
}

.flow-step.done {
  color: #a7f3d0;
}

.flow-index {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-right: 8px;
  font-size: 13px;
  font-weight: 700;
  background: rgba(0, 0, 0, 0.2);
  color: inherit;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  flex: 0 0 auto;
}

.flow-step.active .flow-index {
  background: #3b82f6; 
  border-color: #60a5fa;
  box-shadow: 0 0 12px rgba(59, 130, 246, 0.6);
  color: #ffffff;
}

.flow-step.done .flow-index {
  background: #10b981; 
  border-color: #34d399;
  color: #ffffff;
}

.flow-label {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  flex: 0 1 auto;
}

.flow-done-icon {
  margin-left: 6px;
  color: #10b981;
  font-size: 15px;
  flex: 0 0 auto;
}

.flow-connector {
  flex: 1 1 auto;
  min-width: 8px;
  height: 2px;
  background: rgba(255, 255, 255, 0.12);
  margin: 0 12px;
  border-radius: 999px;
  position: relative;
  overflow: hidden;
  transition: background 0.3s ease;
}

.flow-step.done .flow-connector {
  background: rgba(16, 185, 129, 0.4);
}

.flow-step.has-error .flow-index {
  border-color: #f87171;
  background: rgba(239, 68, 68, 0.2);
  color: #fca5a5;
}

.setup-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: nowrap;
  white-space: nowrap;
  overflow-x: auto;
}
.setup-toolbar::-webkit-scrollbar {
  display: none;
}

.setup-step-errors {
  padding: 8px 12px 0;
  background: #ffffff;
}

.setup-import-errors {
  margin-top: 10px;
}

.setup-import-error-list {
  margin-top: 8px;
  border: 1px solid #f2c5c5;
  background: #fff7f7;
  border-radius: 8px;
  padding: 8px 10px;
  max-height: 220px;
  overflow: auto;
}

.setup-import-error-item {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) 2fr;
  gap: 10px;
  padding: 4px 0;
  border-bottom: 1px dashed #f3d9d9;
}

.setup-import-error-item:last-child {
  border-bottom: none;
}

.setup-import-error-field {
  font-size: 12px;
  color: #9f4646;
}

.setup-import-error-message {
  font-size: 12px;
  color: #c23b3b;
}

.setup-import-error-toggle {
  margin-top: 6px;
}

.setup-toolbar-left {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: nowrap;
}

.setup-version-meta {
  display: flex;
  align-items: center;
  gap: 12px;
}

.meta-item {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  padding: 4px 10px;
  border-radius: 8px;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(8px);
}

.meta-label {
  font-weight: 600;
  color: rgba(255, 255, 255, 0.5);
}

.meta-item:first-child .meta-label {
  color: #a7f3d0;
}

.meta-group {
  display: flex;
  align-items: center;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.05);
}

.meta-br {
  background: rgba(255, 255, 255, 0.08);
  padding: 2px 6px;
  color: #93c5fd;
}

.draft-group .meta-br {
  color: #c4b5fd;
}

.meta-ver {
  padding: 2px 6px;
  font-weight: 700;
  color: #ffffff;
}

.meta-time {
  color: rgba(255, 255, 255, 0.4);
  display: inline-flex;
  align-items: center;
  cursor: help;
  transition: color 0.2s;
  font-size: 14px;
}

.meta-time:hover {
  color: rgba(255, 255, 255, 0.8);
}

.meta-status {
  margin-left: 2px;
  border: none;
  background: rgba(255, 255, 255, 0.1) !important;
}

.locked-tag {
  background: rgba(245, 158, 11, 0.1) !important;
  border-color: rgba(245, 158, 11, 0.3) !important;
}

.setup-view-mode :deep(.el-radio-group) {
  background: rgba(11, 30, 75, 0.24);
  border-radius: 8px;
}

.setup-view-mode :deep(.el-radio-button__inner) {
  border-color: rgba(187, 217, 255, 0.32);
  color: rgba(237, 247, 255, 0.92);
  background: transparent;
}

.setup-view-mode :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-left-color: rgba(187, 217, 255, 0.32);
}

.setup-view-mode :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: rgba(255, 255, 255, 0.18);
  border-color: rgba(227, 240, 255, 0.9);
  color: #ffffff;
  box-shadow: none;
}

.hidden-file-input {
  display: none;
}

.switcher-title {
  font-size: 18px;
  line-height: 1;
  font-weight: 600;
  color: #ffffff;
}

.setup-switcher :deep(.el-button) {
  border-color: rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.92);
}

.setup-toolbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
  padding-top: 2px;
}

.setup-toolbar-actions :deep(.el-button) {
  border-radius: 10px;
}

/* ========== 版本管理对话框 - 美化样式 ========== */

/* 对话框整体样式 */
.version-mgr-dialog :deep(.el-dialog) {
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(15, 30, 60, 0.18), 0 2px 12px rgba(15, 30, 60, 0.08);
}

.version-mgr-dialog :deep(.el-dialog__header) {
  padding: 0;
  margin: 0;
}

.version-mgr-dialog :deep(.el-dialog__body) {
  padding: 0;
}

.version-mgr-dialog :deep(.el-dialog__footer) {
  padding: 12px 20px;
  background: #f8fafc;
  border-top: 1px solid #edf2f7;
}

/* 对话框头部 */
.vm-dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  background: linear-gradient(135deg, #1e3a5f 0%, #2d5a8e 50%, #1a4478 100%);
  color: #ffffff;
}

.vm-dialog-header-left {
  display: flex;
  align-items: center;
  gap: 10px;
}

.vm-dialog-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(8px);
}

.vm-dialog-title {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.vm-version-count-tag {
  background: rgba(255, 255, 255, 0.18) !important;
  border-color: rgba(255, 255, 255, 0.25) !important;
  color: rgba(255, 255, 255, 0.92) !important;
  font-weight: 600;
  font-size: 11px;
  backdrop-filter: blur(4px);
}

.vm-dialog-close-btn {
  background: rgba(255, 255, 255, 0.12) !important;
  border: 1px solid rgba(255, 255, 255, 0.2) !important;
  color: rgba(255, 255, 255, 0.85) !important;
  transition: all 0.2s ease;
}

.vm-dialog-close-btn:hover {
  background: rgba(255, 255, 255, 0.25) !important;
  color: #ffffff !important;
  transform: rotate(90deg);
}

/* 对话框主体 */
.vm-body {
  padding: 16px 20px 12px;
  min-height: 200px;
  background: linear-gradient(180deg, #f0f5fb 0%, #f8fafc 60px, #ffffff 100%);
}

/* 图例栏 */
.vm-legend-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 14px;
  padding: 10px 14px;
  background: #ffffff;
  border-radius: 10px;
  border: 1px solid #e8eef6;
  box-shadow: 0 1px 4px rgba(15, 40, 80, 0.04);
}

.vm-legend-section {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.vm-legend-label {
  font-size: 12px;
  font-weight: 600;
  color: #475569;
  white-space: nowrap;
  padding-right: 4px;
  border-right: 2px solid #e2e8f0;
}

.vm-legend-tags {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.vm-lane-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px 3px 7px;
  font-size: 12px;
  font-weight: 600;
  color: #64748b;
  background: #f1f5f9;
  border-radius: 20px;
  border: 1px solid #e2e8f0;
  transition: all 0.2s ease;
}

.vm-lane-badge:hover {
  background: #e8eff8;
  border-color: #c8d8ea;
}

.vm-lane-badge.is-main {
  color: #1e40af;
  background: linear-gradient(135deg, #eff6ff, #dbeafe);
  border-color: #bfdbfe;
}

.vm-lane-badge-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #94a3b8;
}

.vm-lane-badge.is-main .vm-lane-badge-dot {
  background: #3b82f6;
  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}

/* 轨道头部标签 */
.rollback-axis-lane-head {
  position: relative;
  height: 28px;
  margin: 0 0 8px;
}

.rollback-axis-lane-head-item {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-size: 10px;
  line-height: 1;
  color: #8896aa;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 3px 6px;
  background: #f0f4f8;
  border-radius: 4px;
}

.rollback-axis-lane-head-item.is-main {
  color: #3b6db5;
  background: #e4edf8;
}

/* 时间线滚动区域 */
.rollback-axis-scroll {
  position: relative;
  max-height: 400px;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 0;
}

/* SVG 覆盖层 */
.rollback-axis-overlay {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 1;
}

/* 版本行 */
.rollback-axis-row {
  position: relative;
  z-index: 2;
  display: flex;
  gap: 10px;
  padding: 4px 6px 4px 0;
  border-radius: 10px;
  transition: all 0.22s ease;
}

.rollback-axis-row:hover {
  background: rgba(59, 130, 246, 0.04);
}

.rollback-axis-row.is-current-row {
  background: linear-gradient(90deg, rgba(34, 197, 94, 0.06) 0%, rgba(34, 197, 94, 0.02) 100%);
  box-shadow: inset 3px 0 0 #22c55e;
}

.rollback-axis-row.is-draft-base-row:not(.is-current-row) {
  background: linear-gradient(90deg, rgba(99, 102, 241, 0.05) 0%, rgba(99, 102, 241, 0.01) 100%);
  box-shadow: inset 3px 0 0 #8b9cf7;
}

/* 轨道图形 */
.rollback-axis-graph {
  position: relative;
  min-height: 74px;
  flex: 0 0 auto;
}

/* 轨道竖线段 */
.rollback-axis-lane-segment {
  position: absolute;
  top: -8px;
  bottom: -8px;
  width: 2px;
  background: linear-gradient(180deg, #d4dfe8, #cedbed);
  transform: translateX(-50%);
  opacity: 0.85;
  border-radius: 1px;
}

.rollback-axis-lane-segment.is-main-lane {
  width: 3px;
  background: linear-gradient(180deg, #5b8fd4, #4a7ec8);
  opacity: 0.9;
}

.rollback-axis-lane-segment.is-active-lane {
  width: 2.5px;
  background: linear-gradient(180deg, #818cf8, #6366f1);
  opacity: 0.85;
}

/* SVG 同分支继承连接线 */
.rollback-axis-branch-line {
  stroke: #a5b4fc;
  stroke-width: 3;
  stroke-linecap: round;
  opacity: 0.7;
}

.rollback-axis-branch-line.is-main {
  stroke: #60a5fa;
  stroke-width: 3.5;
  opacity: 0.6;
}

/* SVG 连接线 */
.rollback-axis-spawn-path {
  fill: none;
  stroke: url(#spawnGrad);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.85;
}

.rollback-axis-merge-path {
  fill: none;
  stroke: url(#mergeGrad);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 6 3;
  opacity: 0.85;
}

/* 轨道节点 */
.rollback-axis-dot {
  position: absolute;
  top: 24px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  transform: translateX(-50%);
  background: #94a3b8;
  border: 2.5px solid #ffffff;
  box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.25), 0 2px 4px rgba(0, 0, 0, 0.08);
  z-index: 3;
  transition: all 0.25s ease;
}

.rollback-axis-row:hover .rollback-axis-dot {
  transform: translateX(-50%) scale(1.2);
}

.rollback-axis-dot.is-main {
  background: #3b82f6;
  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3), 0 2px 6px rgba(59, 130, 246, 0.15);
}

.rollback-axis-dot.is-current {
  background: #22c55e;
  width: 16px;
  height: 16px;
  top: 23px;
  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.25), 0 2px 8px rgba(34, 197, 94, 0.2);
  animation: dotPulse 2.5s ease-in-out infinite;
}

@keyframes dotPulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.25), 0 2px 8px rgba(34, 197, 94, 0.2); }
  50% { box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.12), 0 2px 12px rgba(34, 197, 94, 0.3); }
}

.rollback-axis-dot.is-merge {
  background: #f59e0b;
  border-color: #fffbeb;
  box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25), 0 2px 4px rgba(245, 158, 11, 0.1);
}

/* 版本内容区域 */
.rollback-axis-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 0 10px;
  border-bottom: 1px solid #f0f4f8;
  transition: border-color 0.2s ease;
}

.rollback-axis-row:last-child .rollback-axis-content {
  border-bottom: none;
}

.rollback-axis-row:hover .rollback-axis-content {
  border-bottom-color: transparent;
}

/* 顶行标题 */
.rollback-axis-topline {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* 版本号 */
.rollback-axis-version {
  font-size: 17px;
  line-height: 1.15;
  color: #1e293b;
  font-weight: 800;
  letter-spacing: -0.01em;
}

/* 状态徽章 */
.vm-status-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px 2px 6px;
  font-size: 11px;
  font-weight: 600;
  border-radius: 20px;
  line-height: 1.5;
  white-space: nowrap;
}

.vm-badge-published {
  color: #166534;
  background: linear-gradient(135deg, #dcfce7, #bbf7d0);
  border: 1px solid #86efac;
}

.vm-badge-draft {
  color: #3730a3;
  background: linear-gradient(135deg, #e8e5ff, #ddd6fe);
  border: 1px solid #c4b5fd;
}

.vm-badge-merge {
  color: #92400e;
  background: linear-gradient(135deg, #fef3c7, #fde68a);
  border: 1px solid #fcd34d;
}

/* 元数据行 */
.rollback-axis-meta {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12px;
  color: #64748b;
}

.vm-meta-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.vm-meta-item .el-icon {
  color: #94a3b8;
}

/* 操作按钮行 */
.rollback-axis-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 2px;
}

.vm-action-btn {
  border-radius: 6px !important;
  font-size: 12px !important;
  font-weight: 500 !important;
  padding: 4px 10px !important;
  height: 28px !important;
  transition: all 0.2s ease !important;
  border: 1px solid transparent !important;
}

.vm-action-btn .el-icon {
  margin-right: 2px;
}

.vm-action-primary {
  color: #3b82f6 !important;
  background: #eff6ff !important;
  border-color: #bfdbfe !important;
}

.vm-action-primary:hover {
  color: #ffffff !important;
  background: #3b82f6 !important;
  border-color: #3b82f6 !important;
  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.25);
}

.vm-action-danger {
  color: #ef4444 !important;
  background: #fef2f2 !important;
  border-color: #fecaca !important;
}

.vm-action-danger:hover {
  color: #ffffff !important;
  background: #ef4444 !important;
  border-color: #ef4444 !important;
  box-shadow: 0 2px 8px rgba(239, 68, 68, 0.25);
}

.vm-action-default {
  color: #475569 !important;
  background: #f8fafc !important;
  border-color: #e2e8f0 !important;
}

.vm-action-default:hover {
  color: #1e293b !important;
  background: #f1f5f9 !important;
  border-color: #cbd5e1 !important;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}

.vm-action-btn.is-disabled {
  opacity: 0.4 !important;
  cursor: not-allowed !important;
}

/* 对话框尾部 */
.vm-dialog-footer {
  display: flex;
  justify-content: flex-end;
}

.setup-toolbar-actions :deep(.el-button) {
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  background: rgba(255, 255, 255, 0.08) !important;
  border: 1px solid rgba(255, 255, 255, 0.25) !important;
  color: #ffffff !important;
  border-radius: 8px;
  padding: 0 12px;
  height: 32px;
  font-weight: 500;
  white-space: nowrap;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.setup-toolbar-actions :deep(.el-button:hover) {
  background: rgba(255, 255, 255, 0.18) !important;
  border-color: rgba(255, 255, 255, 0.4) !important;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.setup-toolbar-actions :deep(.el-button.is-disabled) {
  opacity: 0.5;
  background: rgba(255, 255, 255, 0.04) !important;
  border-color: rgba(255, 255, 255, 0.1) !important;
  color: rgba(255, 255, 255, 0.4) !important;
  transform: none;
  box-shadow: none;
}

/* 按钮特异性颜色提示 (利用边框和发光，不改变毛玻璃主体颜色) */
.setup-toolbar-actions :deep(.el-button--success.is-plain) {
  border-color: rgba(34, 197, 94, 0.4) !important;
  color: #a7f3d0 !important;
}
.setup-toolbar-actions :deep(.el-button--success.is-plain:hover) {
  background: rgba(34, 197, 94, 0.15) !important;
  border-color: rgba(34, 197, 94, 0.6) !important;
  color: #ffffff !important;
  box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2);
}

.setup-toolbar-actions :deep(.el-button--danger.is-plain) {
  border-color: rgba(239, 68, 68, 0.4) !important;
  color: #fca5a5 !important;
}
.setup-toolbar-actions :deep(.el-button--danger.is-plain:hover) {
  background: rgba(239, 68, 68, 0.15) !important;
  border-color: rgba(239, 68, 68, 0.6) !important;
  color: #ffffff !important;
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}

.setup-toolbar-actions :deep(.el-button--warning.is-plain) {
  border-color: rgba(245, 158, 11, 0.4) !important;
  color: #fde68a !important;
}
.setup-toolbar-actions :deep(.el-button--warning.is-plain:hover) {
  background: rgba(245, 158, 11, 0.15) !important;
  border-color: rgba(245, 158, 11, 0.6) !important;
  color: #ffffff !important;
  box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
}

.secondary-menu-btn {
  border-color: rgba(255, 255, 255, 0.25) !important;
  background: rgba(255, 255, 255, 0.08) !important;
}

/* 模式切换单选按钮美化 */
.setup-view-mode {
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.setup-view-mode :deep(.el-radio-button__inner) {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: none !important;
  padding: 6px 14px;
  font-weight: 500;
  transition: all 0.2s;
}

.setup-view-mode :deep(.el-radio-button:not(:first-child) .el-radio-button__inner) {
  border-left: 0;
}

.setup-view-mode :deep(.el-radio-button__inner:hover) {
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.9);
}

.setup-view-mode :deep(.el-radio-button.is-active .el-radio-button__inner) {
  background: #3b82f6;
  border-color: #60a5fa;
  color: #ffffff;
  font-weight: 600;
}

.setup-section {
  position: relative;
  padding: 8px 12px 12px;
  border-bottom: 1px solid #edf2f8;
  background: #ffffff;
}

.setup-section--table {
  padding-left: 0;
  padding-right: 0;
  padding-top: 0;
}

.enrollment-plan-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.enrollment-plan-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 32px;
  padding: 4px 0 8px 0;
}

.enrollment-inline-group {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.enrollment-inline-label {
  font-size: 13px;
  font-weight: 500;
  color: #64748b;
  background: transparent;
  padding: 0;
  display: inline-flex;
  align-items: center;
  border: none;
}

.enrollment-inline-label:not(:empty)::after {
  content: ":";
  margin-left: 2px;
}

.enrollment-inline-label.required::before {
  content: "*";
  color: #d14141;
  margin-right: 4px;
}

.enrollment-date-cell {
  min-width: auto;
}

.enrollment-date-cell :deep(.el-date-editor.el-input),
.enrollment-date-cell :deep(.el-date-editor) {
  width: 135px !important;
  min-width: 135px !important;
}

.enrollment-target-cell {
  min-width: auto;
}

.enrollment-target-cell :deep(.el-input-number) {
  width: 110px !important;
  min-width: 110px !important;
}

.enrollment-inline-value {
  font-size: 14px;
  font-weight: 500;
  color: #0f172a;
}

.updated-value-wrap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  gap: 4px;
}

.field-updated-tag {
  position: static;
  transform: none;
  left: auto;
  top: auto;
  margin: 0 !important;
  z-index: 1;
  pointer-events: none;
  
  /* 极简指示风格：仅用文字高亮，去背景和边框 */
  background: transparent !important;
  border: none !important;
  color: #ea580c !important; /* 精致的警惕橙 */
  padding: 0 !important;
  height: auto !important;
  line-height: 1 !important;
  font-size: 11px !important;
  font-weight: 600;
  white-space: nowrap;
}

.enrollment-plan-toolbar .updated-value-wrap {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.enrollment-sep {
  color: #94a3b8;
  font-weight: 500;
  margin: 0 4px;
  display: inline-flex;
  align-items: center;
  height: 28px;
  line-height: 28px;
  flex-shrink: 0;
}

.enrollment-plan-summary {
  border: none;
  border-radius: 0;
  background: transparent;
  padding: 12px 0;
  border-top: 1px solid #f1f5f9;
  border-bottom: 1px solid #f1f5f9;
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.enrollment-summary-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  font-size: 13px;
  color: #64748b;
}

.enrollment-summary-line b {
  color: #0f172a;
  font-weight: 600;
  font-size: 14px;
  margin: 0 4px;
}

.summary-site-name {
  font-weight: 700;
  color: #11264d;
}

.summary-title {
  font-weight: 600;
  color: #0f172a;
  min-width: 90px;
  margin-right: -4px;
}

.summary-danger {
  color: #d14141;
  font-weight: 600;
}

.summary-success {
  color: #2f7a4d;
  font-weight: 600;
}

.enrollment-plan-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}

.site-plan-selector {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-right: 12px;
}

.enrollment-plan-action-link {
  color: #5f7499;
  font-weight: 600;
}

.enrollment-plan-action-link:hover {
  color: #3f617d;
}

.enrollment-month-plan-table :deep(th.el-table__cell .cell) {
  text-align: center;
}

.enrollment-month-plan-table :deep(td.el-table__cell .cell) {
  text-align: center;
  padding-top: 5px;
  padding-bottom: 5px;
}

.enrollment-period-plan-table :deep(th.el-table__cell .cell) {
  text-align: center;
}

.enrollment-period-plan-table :deep(td.el-table__cell .cell) {
  text-align: center;
  padding-top: 6px;
  padding-bottom: 6px;
}

.month-target-input {
  width: 82px;
}

.month-empty {
  color: #64748b;
  font-size: 14px;
  text-align: center;
  padding: 40px 0;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px dashed #e2e8f0;
  margin-top: 12px;
}

.site-plan-site-select {
  width: 200px;
  max-width: 100%;
}

.setup-content {
  position: relative;
  padding-left: 0 !important;
  padding-right: 0 !important;
  padding-bottom: 0 !important;
}

.setup-content-step-title {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  font-size: 18px;
  font-weight: 700;
  color: #162f57;
  border-bottom: 1px solid #e8eff8;
  background: #ffffff;
}

.setup-content-step-nav {
  display: flex;
  align-items: center;
  gap: 6px;
}

.setup-content-step-nav :deep(.el-button.is-circle) {
  width: 32px;
  height: 32px;
  padding: 0;
  background: #f1f5f9 !important;
  border: 1.5px solid #cbd5e1 !important;
  color: #475569 !important;
  font-size: 14px;
  transition: all 0.2s;
}

.setup-content-step-nav :deep(.el-button.is-circle:hover) {
  background: #e2e8f0 !important;
  border-color: #94a3b8 !important;
  color: #1e40af !important;
}

.setup-content-step-nav :deep(.el-button.is-circle.is-disabled),
.setup-content-step-nav :deep(.el-button.is-circle.is-disabled:hover) {
  background: #f8fafc !important;
  border-color: #e2e8f0 !important;
  color: #cbd5e1 !important;
  cursor: not-allowed;
}

.setup-content-step-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.setup-content-step-actions :deep(.step-action-btn) {
  min-width: 64px;
  height: 28px;
  padding: 0 12px;
  border-radius: 6px;
  font-weight: 500;
  font-size: 13px;
  transition: all 0.2s;
}

.setup-content-step-actions :deep(.step-action-btn.el-button--primary) {
  background: transparent !important;
  border: 1px solid #3b82f6 !important;
  color: #3b82f6 !important;
}

.setup-content-step-actions :deep(.step-action-btn.el-button--primary:hover) {
  background: #eff6ff !important;
  border-color: #2563eb !important;
  color: #2563eb !important;
}

.setup-content-step-actions :deep(.step-action-btn-cancel) {
  background: #f8fafc !important;
  border: 1px solid #e2e8f0 !important;
  color: #64748b !important;
}

.setup-content-step-actions :deep(.step-action-btn-cancel:hover) {
  background: #f1f5f9 !important;
  border-color: #cbd5e1 !important;
  color: #334155 !important;
}

.setup-content-step-actions :deep(.step-action-btn.el-button--primary .el-button__text),
.setup-content-step-actions :deep(.step-action-btn.el-button--primary > span),
.setup-content-step-actions :deep(.step-action-btn.el-button--primary .el-button__inner) {
  color: inherit !important;
  opacity: 1 !important;
}

.setup-edit-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1200;
  background: rgba(10, 20, 40, 0.45);
  backdrop-filter: blur(2px);
  animation: setup-backdrop-fade-in 0.2s ease-out;
}

.setup-content.setup-content-drawer {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 1300;
  width: min(380px, 100vw);
  height: 100vh;
  overflow-y: auto;
  background: #ffffff;
  box-shadow: -12px 0 36px rgba(7, 22, 50, 0.22);
  display: flex;
  flex-direction: column;
  animation: setup-drawer-slide-in 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
  will-change: transform, opacity;
}

.setup-edit-backdrop.is-closing {
  animation: setup-backdrop-fade-out 0.2s ease-in forwards;
}

.setup-content.setup-content-drawer.is-closing {
  animation: setup-drawer-slide-out 0.22s ease-in forwards;
}

.setup-content.setup-content-drawer .setup-section {
  border-bottom: 1px solid #edf2f8;
  border-radius: 0;
  padding: 12px 16px;
}

.setup-drawer-header {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid #e6edf7;
  background: #ffffff;
}

.setup-drawer-title-wrap {
  display: flex;
  align-items: center;
  min-height: 32px;
}

.setup-drawer-title {
  font-size: 18px;
  font-weight: 700;
  color: #11264d;
}

.setup-drawer-footer {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid #e6edf7;
  background: #ffffff;
}

.drawer-form-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.drawer-form-card {
  padding: 16px 0;
  border: none;
  border-bottom: 1px solid #f1f5f9;
  background: transparent;
  border-radius: 0;
}

.drawer-form-card:last-child {
  border-bottom: none;
}

.drawer-form-card :deep(.el-form-item) {
  margin-bottom: 10px;
}

.drawer-form-card :deep(.el-form-item:last-child) {
  margin-bottom: 0;
}

.drawer-form-card-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding-top: 4px;
}

@keyframes setup-drawer-slide-in {
  from {
    transform: translateX(100%);
    opacity: 0.9;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes setup-backdrop-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes setup-drawer-slide-out {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(100%);
    opacity: 0.94;
  }
}

@keyframes setup-backdrop-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.field-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.field-error {
  font-size: 12px;
  line-height: 1.3;
  color: #d14141;
}

.setup-section:last-of-type {
  border-bottom: 0;
}

.setup-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  margin-bottom: 10px;
}

.setup-section--table .setup-section-header {
  margin-bottom: 0;
  padding-left: 12px;
  padding-right: 12px;
}

.setup-content.setup-content-drawer .setup-section-header {
  padding-bottom: 0;
  margin-bottom: 0;
  min-height: 0;
}

.published-view-tip {
  margin-bottom: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid #cfe1ff;
  background: #f4f8ff;
  color: #35527d;
  font-size: 12px;
}

.section-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--ctms-text-main);
}

.w-full {
  width: 100%;
}

.info-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px 24px; /* 进一步压缩行间距 */
}

.basic-info-groups {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.info-group {
  padding: 16px 0;
  background: transparent;
  border: none;
  border-bottom: 1px solid #eaeff8;
  border-radius: 0;
  box-shadow: none;
}

.info-group:last-child {
  border-bottom: none;
}

.info-group + .info-group {
  margin-top: 0;
}

.info-group-title {
  font-size: 15px;
  font-weight: 700;
  color: #0f172a;
  margin-bottom: 12px;
  display: flex;
  align-items: center;
}

.info-group-title::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 14px;
  background: #3b82f6;
  border-radius: 4px;
  margin-right: 8px;
}

.info-group-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.info-group-title-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.info-group-edit-btn {
  min-width: 64px;
  height: 28px;
  padding: 0 12px;
  border-radius: 6px;
  background: transparent !important;
  border: 1px solid #3b82f6 !important;
  color: #3b82f6 !important;
  font-size: 13px !important;
  font-weight: 500 !important;
  transition: all 0.2s;
}

.info-group-edit-btn:deep(span) {
  font-size: 13px;
  line-height: 1;
  color: #3b82f6 !important;
  opacity: 1 !important;
}

.info-group-edit-btn:hover,
.info-group-edit-btn:focus {
  background: #eff6ff !important;
  border-color: #2563eb !important;
  color: #2563eb !important;
}

.info-group-edit-btn:hover:deep(span),
.info-group-edit-btn:focus:deep(span) {
  color: #2563eb !important;
}

.info-group-edit-btn.is-disabled,
.info-group-edit-btn.is-disabled:hover {
  background: transparent !important;
  border-color: #cbd5e1 !important;
  color: #94a3b8 !important;
}

.info-group-edit-btn.is-disabled:deep(span),
.info-group-edit-btn.is-disabled:hover:deep(span) {
  color: #94a3b8 !important;
}

.basic-form-group-title {
  margin: 0 0 14px;
  font-size: 14px;
  font-weight: 700;
  color: #0f172a;
  display: flex;
  align-items: center;
}
.basic-form-group-title::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 14px;
  background: #3b82f6;
  border-radius: 4px;
  margin-right: 8px;
}

.basic-edit-group {
  padding: 20px 0 4px;
  background: transparent;
  border: none;
  border-bottom: 1px solid #eaeff8;
  border-radius: 0;
  margin-bottom: 0;
}

.basic-edit-group:last-child {
  border-bottom: none;
}

.basic-edit-group + .basic-edit-group {
  margin-top: 0;
  padding-top: 20px;
  border-top: none;
}

.info-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 28px;
  padding: 2px 0; /* 大幅精简项内垂直留白 */
}

.info-label {
  color: #64748b;
  min-width: 100px;
  font-size: 13px;
  background: transparent;
  padding: 2px 0;
  display: inline-flex;
  align-items: center;
}

.info-value {
  color: #0f172a;
  font-weight: 500;
  font-size: 14px;
  word-break: break-word;
  padding-top: 1px;
}

.info-value-wrap {
  display: inline-block;
  position: relative;
  vertical-align: middle;
}

.setup-info-tabs {
  margin-top: -8px;
  position: relative;
}

.setup-section :deep(.el-tabs__item) {
  font-size: 15px;
  font-weight: 600;
}

.setup-section :deep(.el-tabs__nav-wrap::after) {
  background-color: #edf2f8;
}

.setup-section :deep(.el-table),
.setup-section :deep(.el-table__inner-wrapper) {
  border-radius: 0;
  overflow: visible;
  border: none !important;
  background: transparent !important;
  box-shadow: none !important;
  --el-table-border-color: #f1f5f9;
}

.setup-section :deep(.el-table::before),
.setup-section :deep(.el-table::after),
.setup-section :deep(.el-table__inner-wrapper::before),
.setup-section :deep(.el-table__inner-wrapper::after) {
  display: none !important; /* 去除原本表格底部及两侧的系统原生粗线 */
}

.setup-section :deep(.el-table th.el-table__cell) {
  background: #f7f9fc !important;
  color: #34506f !important;
  font-weight: 600 !important;
  height: 40px;
  border-bottom: 1px solid #e2e8f0 !important;
  border-right: none !important;
  border-left: none !important;
}

.setup-section :deep(.el-table td.el-table__cell) {
  border-bottom: 1px solid #f1f5f9 !important;
  border-right: none !important;
  border-left: none !important;
  background: transparent !important;
  padding-top: 7px;
  padding-bottom: 7px;
}

/* 覆盖悬停状态颜色，保持极简高级 */
.setup-section :deep(.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell) {
  background-color: #f8fafc;
}

.setup-milestone-table :deep(th.el-table__cell),
.setup-milestone-table :deep(td.el-table__cell) {
  height: 36px;
}

.setup-milestone-table :deep(th.el-table__cell .cell),
.setup-milestone-table :deep(td.el-table__cell .cell) {
  padding-top: 4px;
  padding-bottom: 4px;
}

.setup-milestone-table :deep(th.el-table__cell .cell) {
  font-size: 13px;
  font-weight: 500;
  color: #64748b;
}

.milestone-cell-text {
  display: inline-block;
  font-size: 13px;
  font-weight: 500;
  color: #0f172a;
  line-height: 16px;
}

.milestone-plan-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.milestone-plan-line {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  line-height: 16px;
  color: #000;
}

.milestone-plan-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 3px solid #2f84c6;
  box-sizing: border-box;
}

.milestone-plan-label {
  color: #4b5563;
}

.milestone-actions-inline {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}

/* ========== 编辑抽屉 - 头部 ========== */
.sme-header {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sme-header-title {
  font-size: 20px;
  font-weight: 700;
  color: #11264d;
  line-height: 1.2;
}

.sme-header-subtitle {
  font-size: 13px;
  color: var(--ctms-text-secondary);
  font-weight: 400;
}

/* ========== 编辑抽屉 - 表单 ========== */
.sme-form {
  padding: 4px 4px 0 0;
}

/* ========== 编辑抽屉 - 分组卡片 ========== */
.sme-group {
  border: 1px solid #e8eef6;
  border-radius: 10px;
  padding: 16px 18px 8px;
  background: #fbfcfe;
  transition: border-color 0.2s ease;
}

.sme-group:hover {
  border-color: #d0dced;
}

.sme-group + .sme-group {
  margin-top: 14px;
}

.sme-group-title {
  font-size: 14px;
  font-weight: 700;
  margin-bottom: 14px;
  color: #1a3560;
  display: flex;
  align-items: center;
  gap: 8px;
}

.sme-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.sme-dot-blue {
  background: #3b82f6;
}

.sme-dot-amber {
  background: #f0ad2c;
}

.sme-dot-green {
  background: #22c55e;
}

.sme-dot-gray {
  background: #909399;
}

/* ========== 编辑抽屉 - 耗时展示 ========== */
.sme-duration-line {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0 8px;
}

.sme-duration-label {
  font-size: 13px;
  color: #6b7280;
  font-weight: 500;
}

.sme-duration-value {
  font-size: 14px;
  font-weight: 700;
  color: #1a3560;
}

/* ========== 编辑抽屉 - 底部按钮 ========== */
.sme-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

/* ========== 编辑抽屉 - 表单元素微调 ========== */
.sme-form :deep(.el-form-item) {
  margin-bottom: 12px;
}

.sme-form :deep(.el-form-item__label) {
  font-size: 13px;
  font-weight: 600;
  color: #4a6283;
  padding-bottom: 4px;
}

/* 兼容旧类名（保留不影响） */
.setup-milestone-editor-header {
  font-size: 22px;
  font-weight: 700;
  color: #11264d;
}

.setup-milestone-editor-form {
  padding-right: 4px;
}

.setup-milestone-editor-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.setup-content.is-published-preview .drawer-form-card-actions {
  display: none;
}

.setup-content.is-published-preview :deep(.el-input.is-disabled .el-input__wrapper),
.setup-content.is-published-preview :deep(.el-textarea.is-disabled .el-textarea__inner),
.setup-content.is-published-preview :deep(.el-select .el-select__wrapper.is-disabled),
.setup-content.is-published-preview :deep(.el-input-number.is-disabled .el-input__wrapper),
.setup-content.is-draft-preview :deep(.el-input.is-disabled .el-input__wrapper),
.setup-content.is-draft-preview :deep(.el-textarea.is-disabled .el-textarea__inner),
.setup-content.is-draft-preview :deep(.el-select .el-select__wrapper.is-disabled),
.setup-content.is-draft-preview :deep(.el-input-number.is-disabled .el-input__wrapper) {
  background: transparent;
  box-shadow: none;
  border: 0;
  padding-left: 0;
  padding-right: 0;
}

.setup-content.is-published-preview :deep(.el-input.is-disabled .el-input__inner),
.setup-content.is-published-preview :deep(.el-textarea.is-disabled .el-textarea__inner),
.setup-content.is-published-preview :deep(.el-select .el-select__selected-item),
.setup-content.is-published-preview :deep(.el-input-number.is-disabled .el-input__inner),
.setup-content.is-draft-preview :deep(.el-input.is-disabled .el-input__inner),
.setup-content.is-draft-preview :deep(.el-textarea.is-disabled .el-textarea__inner),
.setup-content.is-draft-preview :deep(.el-select .el-select__selected-item),
.setup-content.is-draft-preview :deep(.el-input-number.is-disabled .el-input__inner) {
  color: #11264d;
  -webkit-text-fill-color: #11264d;
  font-weight: 600;
}

.setup-content.is-published-preview :deep(.el-input.is-disabled .el-input__suffix),
.setup-content.is-published-preview :deep(.el-select .el-select__caret),
.setup-content.is-published-preview :deep(.el-input-number__decrease),
.setup-content.is-published-preview :deep(.el-input-number__increase),
.setup-content.is-draft-preview :deep(.el-input.is-disabled .el-input__suffix),
.setup-content.is-draft-preview :deep(.el-select .el-select__caret),
.setup-content.is-draft-preview :deep(.el-input-number__decrease),
.setup-content.is-draft-preview :deep(.el-input-number__increase) {
  display: none;
}

.detail-form :deep(.el-input),
.detail-form :deep(.el-select),
.detail-form :deep(.el-input-number) {
  width: 100%;
}

.setup-content.setup-content-drawer :deep(.el-row) {
  margin-left: 0 !important;
  margin-right: 0 !important;
  display: block;
}

.setup-content.setup-content-drawer :deep(.el-row > .el-col) {
  max-width: 100% !important;
  flex: 0 0 100% !important;
  width: 100% !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
}

.publish-incomplete {
  margin-bottom: 12px;
}

.publish-incomplete-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.conflict-summary {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
  color: #334a75;
  font-size: 13px;
}

.conflict-diff-lines {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.conflict-readable-diff {
  margin-bottom: 12px;
}

.publish-diff-overview {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.summary-section-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  margin: 10px 0 8px;
}

.summary-display-group {
  padding-top: 16px;
}

.visit-schedule-display {
  width: 100%;
  border: 1px solid #e1e9f3;
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
}

.visit-schedule-display-head,
.visit-schedule-display-row {
  display: grid;
  grid-template-columns: minmax(160px, 0.8fr) minmax(160px, 0.8fr) minmax(220px, 1fr);
  gap: 16px;
  align-items: center;
}

.visit-schedule-display-head {
  min-height: 40px;
  padding: 0 14px;
  background: #f3f7fc;
  border-bottom: 1px solid #dce6f2;
  color: #35516f;
  font-size: 13px;
  font-weight: 800;
}

.visit-schedule-display-row {
  min-height: 42px;
  padding: 0 14px;
  border-bottom: 1px solid #edf2f7;
  color: #334155;
  font-size: 13px;
}

.visit-schedule-display-row:last-of-type {
  border-bottom: none;
}

.visit-schedule-display-code {
  color: #0f172a;
  font-weight: 700;
}

.visit-schedule-display-empty {
  padding: 18px 14px;
  color: #94a3b8;
  font-size: 13px;
  text-align: center;
}

.visit-schedule-editor {
  width: 100%;
  border: 1px solid #e1e9f3;
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
}

.visit-schedule-head,
.visit-schedule-row {
  display: grid;
  grid-template-columns: minmax(220px, 0.9fr) minmax(210px, 1fr) minmax(150px, 0.75fr) minmax(150px, 0.75fr) 48px;
  gap: 12px;
  align-items: center;
}

.visit-schedule-head {
  min-height: 40px;
  padding: 0 12px;
  background: #f3f7fc;
  border-bottom: 1px solid #dce6f2;
  color: #35516f;
  font-size: 13px;
  font-weight: 800;
}

.visit-schedule-row {
  position: relative;
  padding: 9px 12px;
  border-bottom: 1px solid #edf2f7;
  cursor: grab;
  transition: background-color 0.16s ease, box-shadow 0.16s ease, opacity 0.16s ease;
}

.visit-schedule-row:active {
  cursor: grabbing;
}

.visit-schedule-row:last-child {
  border-bottom: none;
}

.visit-schedule-row.is-dragging {
  background: #f8fbff;
  opacity: 0.62;
}

.visit-schedule-row.insert-before::before,
.visit-schedule-row.insert-after::after {
  position: absolute;
  left: 12px;
  right: 12px;
  z-index: 2;
  height: 3px;
  border-radius: 999px;
  background: #2f80ed;
  box-shadow: 0 0 0 3px rgba(47, 128, 237, 0.14);
  content: "";
}

.visit-schedule-row.insert-before::before {
  top: -2px;
}

.visit-schedule-row.insert-after::after {
  bottom: -2px;
}

.visit-schedule-row.insert-before,
.visit-schedule-row.insert-after {
  background: #f8fbff;
}

.visit-code-field {
  min-width: 0;
}

.visit-field {
  min-width: 0;
}

.visit-field-label {
  display: none;
  margin-bottom: 5px;
  color: #64748b;
  font-size: 12px;
  font-weight: 700;
}

.visit-schedule-remove {
  justify-self: center;
  width: 32px;
  height: 32px;
}

.visit-schedule-add {
  margin-top: 0;
}

.visit-special-rule {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid #dce6f2;
  border-radius: 8px;
  background: #f8fbff;
}

.visit-special-rule-title {
  color: #0f172a;
  font-size: 13px;
  font-weight: 800;
}

.visit-special-rule-body {
  margin-top: 6px;
  color: #475569;
  font-size: 13px;
  line-height: 1.6;
}

.visit-schedule-dialog :deep(.el-dialog) {
  border-radius: 10px;
}

.visit-schedule-dialog :deep(.el-dialog__header) {
  padding: 18px 20px 12px;
  margin: 0;
  border-bottom: 1px solid #e6edf7;
}

.visit-schedule-dialog :deep(.el-dialog__title) {
  color: #0f172a;
  font-size: 18px;
  font-weight: 700;
}

.visit-schedule-dialog :deep(.el-dialog__body) {
  padding: 14px 20px 18px;
}

.visit-schedule-dialog :deep(.el-dialog__footer) {
  padding: 12px 20px;
  border-top: 1px solid #e6edf7;
}

.visit-schedule-dialog-form .summary-section-bar {
  margin-top: 0;
}

.visit-schedule-dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.conflict-readable-diff :deep(.el-table th.el-table__cell) {
  background: #f7faff;
  color: #2f4672;
}

@media (max-width: 960px) {
  .setup-switcher {
    width: 100%;
  }

  .version-card {
    width: 100%;
    min-width: 0;
  }

  .setup-toolbar-actions {
    width: 100%;
  }

  .info-grid {
    grid-template-columns: 1fr;
  }

  .visit-schedule-head {
    display: none;
  }

  .visit-schedule-display-head {
    display: none;
  }

  .visit-schedule-display-row {
    grid-template-columns: 1fr;
    gap: 4px;
    padding: 10px 14px;
  }

  .visit-schedule-row {
    grid-template-columns: 1fr;
    gap: 10px;
  }

  .visit-field-label {
    display: block;
  }

  .visit-schedule-remove {
    justify-self: flex-end;
  }

  .flow-label {
    font-size: 14px;
  }

  .switcher-title {
    font-size: 16px;
  }

}
</style>
