PlanFormContainer.vue 6.78 KB
<template>
  <!-- 使用 PlanPopup 容器组件 -->
  <PlanPopup
    :visible="props.visible"
    :title="templateConfig?.name || '计划书'"
    @close="close"
    @submit="submit"
  >
    <!-- 动态加载模版组件 -->
    <component
      :is="currentTemplateComponent"
      ref="templateRef"
      v-model="formData"
      :config="templateConfig?.config"
      v-if="currentTemplateComponent && templateConfig?.config"
    />

    <!-- 错误提示 -->
    <div v-else class="text-center text-gray-500 py-10">
      <p>⚠️ 未找到对应的计划书模版</p>
      <p class="text-sm mt-2">form_sn: {{ product?.form_sn }}</p>
    </div>
  </PlanPopup>
</template>

<script setup>
/**
 * 计划书表单容器
 *
 * @description 根据产品的 form_sn 动态加载对应的计划书模版组件
 *              - 自动识别产品并加载模版
 *              - 支持后端 plan_config 动态配置
 *              - 统一的表单提交处理
 * @author Claude Code
 * @example
 * <PlanFormContainer
 *   v-model:visible="showPlanPopup"
 *   :product="selectedProduct"
 *   @close="handleClose"
 *   @submit="handleSubmit"
 * />
 */
import { ref, computed, watch, nextTick } from 'vue'
import PlanPopup from './PlanPopup/index.vue'
import LifeInsuranceTemplate from './PlanTemplates/LifeInsuranceTemplate.vue'
import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue'
import SavingsTemplate from './PlanTemplates/SavingsTemplate.vue'
import { PLAN_TEMPLATES } from '@/config/plan-templates'

/**
 * 组件属性
 */
const props = defineProps({
  /**
   * 是否显示弹窗
   * @type {boolean}
   */
  visible: {
    type: Boolean,
    default: false
  },

  /**
   * 产品对象
   * @type {Object}
   * @property {number} id - 产品 ID
   * @property {string} product_name - 产品名称
   * @property {string} form_sn - 模版标识(必需)
   * @property {Object} plan_config - 模版配置(可选,后端返回)
   */
  product: {
    type: Object,
    required: false,
    default: null
  }
})

/**
 * 组件事件
 */
const emit = defineEmits([
  /**
   * 更新显示状态事件
   * @event update:visible
   * @param {boolean} value - 显示状态
   */
  'update:visible',

  /**
   * 关闭事件
   * @event close
   */
  'close',

  /**
   * 提交事件
   * @event submit
   * @param {Object} formData - 表单数据
   */
  'submit'
])

/**
 * 当前模版配置
 * @description 根据 form_sn 从配置文件中查找,并合并后端 plan_config
 *
 * @example
 * // product.form_sn = 'life-insurance-wiop3e'
 * // templateConfig() 返回: {
 * //   name: 'WIOP3E...',
 * //   component: 'LifeInsuranceTemplate',
 * //   config: { currency: 'USD', ... }
 * // }
 */
const templateConfig = computed(() => {
  if (!props.product) {
    return null
  }

  if (!props.product.form_sn) {
    console.warn('[PlanFormContainer] 产品缺少 form_sn 字段', props.product)
    return null
  }

  // 从配置文件中查找模版
  const config = PLAN_TEMPLATES[props.product.form_sn]
  if (!config) {
    console.error(`[PlanFormContainer] 未找到模版配置: ${props.product.form_sn}`)
    return null
  }

  // 合并配置:优先使用后端返回的 plan_config,否则使用配置文件中的默认配置
  return {
    ...config,
    config: {
      ...config.config,
      ...(props.product.plan_config || {})
    }
  }
})

/**
 * 当前模版组件
 * @description 根据 component 名称动态加载对应的组件
 */
const currentTemplateComponent = computed(() => {
  if (!templateConfig.value) {
    return null
  }

  const componentMap = {
    'LifeInsuranceTemplate': LifeInsuranceTemplate,
    'CriticalIllnessTemplate': CriticalIllnessTemplate,
    'SavingsTemplate': SavingsTemplate
  }

  const componentName = templateConfig.value.component
  return componentMap[componentName] || null
})

/**
 * 表单数据
 */
const formData = ref({})

/**
 * 模版组件引用
 */
const templateRef = ref(null)

/**
 * 监听显示状态变化,弹窗打开时确保表单是干净的
 * @description 这是最后的安全网,确保弹窗打开时表单一定是空的
 */
watch(
  () => props.visible,
  (newVal) => {
    if (newVal && Object.keys(formData.value).length > 0) {
      // 弹窗打开且表单有数据时,强制重置
      console.log('[PlanFormContainer] 弹窗打开时检测到残留数据,强制重置')
      resetForm()
    }
  }
)

/**
 * 关闭弹窗
 * @description 关闭时重置表单数据,避免下次打开时保留旧数据
 *
 * ⚠️ 重要:必须使用 nextTick 延迟重置
 * 原因:避免响应式更新时序问题,确保子组件完全卸载后再重置数据
 *
 * 时序问题示例:
 * 1. close() → resetForm() → emit('close')
 * 2. emit('close') → 父组件设置 visible = false
 * 3. 子组件开始卸载(异步)
 * 4. ⚠️ 如果在步骤3之前就重置,子组件可能还保留旧数据
 *
 * 解决方案:
 * 1. 先触发关闭事件(让父组件更新 visible)
 * 2. 等待 nextTick(确保 DOM 更新完成)
 * 3. 再重置表单数据
 */
const close = async () => {
  console.log('[PlanFormContainer] 关闭弹窗,准备重置表单')

  // ⚠️ 关键:先触发关闭事件,让父组件更新 visible
  emit('close')

  // 等待 Vue 的响应式更新完成(确保子组件开始卸载)
  await nextTick()

  // 现在重置表单,确保不会被子组件保留的引用覆盖
  resetForm()

  console.log('[PlanFormContainer] 弹窗已关闭,表单已重置')
}

/**
 * 提交表单
 * @description 将表单数据和产品信息一起提交
 */
const submit = async () => {
  if (!props.product) {
    console.error('[PlanFormContainer] 无法提交: 产品数据为空')
    return
  }

  // 调用模版组件的校验方法
  if (templateRef.value && templateRef.value.validate) {
    const isValid = templateRef.value.validate()
    if (!isValid) {
      return
    }
  }

  console.log('[PlanFormContainer] 提交计划书:', {
    product_id: props.product.id,
    product_name: props.product.product_name,
    form_sn: props.product.form_sn,
    form_data: formData.value
  })

  // 发送提交事件
  emit('submit', {
    product_id: props.product.id,
    form_sn: props.product.form_sn,
    form_data: formData.value
  })

  // ⚠️ 等待父组件处理提交事件(可能需要关闭弹窗)
  await nextTick()

  // 提交成功后重置表单,避免下次打开时保留旧数据
  resetForm()
}

/**
 * 重置表单数据
 * @description 清空表单数据和错误状态
 */
const resetForm = () => {
  console.log('[PlanFormContainer] 重置表单数据')

  // 重置表单数据
  formData.value = {}

  // 重置子组件的验证状态(如果有)
  if (templateRef.value && templateRef.value.clearErrors) {
    templateRef.value.clearErrors()
  }
}
</script>

<style lang="less">
/* 容器样式 */
</style>