LifeInsuranceTemplate.vue 7.59 KB
<template>
  <div v-if="config">
    <!-- 客户姓名 -->
    <PlanFieldName
      v-model="form.customer_name"
      label="客户姓名"
      placeholder="请输入客户姓名"
      :required="true"
      class="mb-5"
    />

    <!-- 性别 -->
    <PlanFieldRadio
      v-model="form.gender"
      label="性别"
      :options="['男', '女']"
      :required="true"
      class="mb-5"
    />

    <!-- 年龄(主字段,选择后自动计算出生年月日) -->
    <PlanFieldAgePicker
      v-model="form.age"
      label="年龄"
      placeholder="请选择年龄"
      :required="true"
      @change="onAgeChange"
      class="mb-5"
    />

    <!-- 出生年月日(根据年龄自动计算,可手动调整) -->
    <PlanFieldDatePicker
      v-model="form.birthday"
      label="出生年月日"
      placeholder="请选择年月日"
      :required="true"
      @change="onBirthdayChange"
      class="mb-5"
    />

    <!-- 是否吸烟 -->
    <PlanFieldRadio
      v-model="form.smoker"
      label="是否吸烟"
      :options="['是', '否']"
      :required="true"
      class="mb-5"
    />

    <!-- 保额(年缴保费) -->
    <PlanFieldAmount
      v-model="form.coverage"
      label="年缴保费"
      placeholder="请输入年缴保费"
      :currency="config.currency"
      :required="true"
      class="mb-5"
    />

    <!-- 缴费年期 -->
    <PlanFieldSelect
      v-model="form.payment_period"
      label="缴费年期"
      placeholder="请选择缴费年期"
      :options="config.payment_periods"
      :required="true"
      class="mb-5"
    />
  </div>

  <!-- 配置缺失提示 -->
  <div v-else class="text-center text-gray-500 py-10">
    <p>⚠️ 模版配置未找到</p>
    <p class="text-sm mt-2">请检查产品配置或联系开发人员</p>
  </div>
</template>

<script setup>
/**
 * 人寿保险计划书模版
 *
 * @description WIOP3E/WIOP3 等人寿保险产品的计划书录入表单
 *              - 支持出生日期自动计算年龄
 *              - 表单字段:性别、年龄、出生年月日、是否吸烟、保额、缴费年期
 * @author Claude Code
 * @example
 * <LifeInsuranceTemplate
 *   v-model="formData"
 *   :config="templateConfig"
 * />
 */
import { reactive, watch, toRefs } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldName from '../PlanFields/NameInput.vue'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'

/**
 * 组件属性
 */
const props = defineProps({
  /**
   * 表单数据对象
   * @type {Object}
   */
  modelValue: {
    type: Object,
    default: () => ({})
  },

  /**
   * 模版配置
   * @type {Object}
   * @property {string} currency - 币种代码
   * @property {Array<string>} payment_periods - 缴费年期选项
   * @property {Object} age_range - 年龄范围 { min, max }
   * @property {string} insurance_period - 保险期间
   */
  config: {
    type: Object,
    required: true
  }
})

/**
 * 组件事件
 */
const emit = defineEmits([
  /**
   * 更新表单数据事件
   * @event update:modelValue
   * @param {Object} value - 表单数据
   */
  'update:modelValue'
])

/**
 * 表单数据
 * @type {Object}
 *
 * ⚠️ 重要:处理父组件重置表单的情况
 * 问题:reactive() 只在初始化时赋值,父组件重置时子组件不会自动更新
 *
 * 解决方案:使用 watch 监听,但只在重置时(空对象)才清空
 * - 判断重置的标准:从有数据变为空对象
 * - 用户输入时的更新:只合并新字段,不删除已有字段
 */
const form = reactive({})

let previousModelValue = null

// 监听父组件的数据变化
watch(
  () => props.modelValue,
  (newVal) => {
    if (!newVal) {
      // null 或 undefined:清空
      Object.keys(form).forEach(key => delete form[key])
      previousModelValue = null
      return
    }

    // 判断是否是重置(从有数据变为空对象)
    const isReset = previousModelValue &&
                    Object.keys(previousModelValue).length > 0 &&
                    Object.keys(newVal).length === 0

    if (isReset) {
      // 父组件重置了:清空表单
      Object.keys(form).forEach(key => delete form[key])
      previousModelValue = newVal
    } else {
      // 正常更新:合并新字段,不删除已有字段
      // 这很重要!因为用户可能刚填写了某些字段,其他字段还没更新
      Object.keys(newVal).forEach(key => {
        form[key] = newVal[key]
      })
      previousModelValue = newVal
    }
  },
  { immediate: true }
)

/**
 * 监听表单数据变化,同步到父组件
 */
watch(
  () => form,
  (newVal) => emit('update:modelValue', newVal),
  { deep: true }
)

/**
 * 年龄变化时自动计算出生年月日
 * @param {number} age - 年龄
 *
 * @description 用户选择年龄后,自动计算并填充出生日期字段
 *              计算公式:当前年份 - 年龄 = 出生年份(默认1月1日)
 */
const onAgeChange = (age) => {
  if (age !== undefined && age !== null && age !== '') {
    const currentYear = new Date().getFullYear()
    const birthYear = currentYear - age

    // 格式化为 YYYY-MM-DD,默认使用1月1日
    const month = String(1).padStart(2, '0')
    const day = String(1).padStart(2, '0')
    form.birthday = `${birthYear}-${month}-${day}`
  }
}

/**
 * 出生日期变化时自动计算年龄
 * @param {string} birthday - 出生日期(格式:YYYY-MM-DD)
 *
 * @description 用户选择出生日期后,自动计算并填充年龄字段
 *              计算公式:当前年份 - 出生年份
 */
const onBirthdayChange = (birthday) => {
  if (birthday) {
    // 兼容 iOS 的日期格式 (YYYY/MM/DD)
    const dateStr = birthday.replace(/-/g, '/')
    const birthDate = new Date(dateStr)

    if (!Number.isNaN(birthDate.getTime())) {
      const birthYear = birthDate.getFullYear()
      const currentYear = new Date().getFullYear()
      const calculatedAge = currentYear - birthYear

      // 自动填充年龄字段(确保非负)
      form.age = Math.max(0, calculatedAge)
    }
  }
}

/**
 * 表单校验
 * @returns {boolean} 是否通过校验
 */
const validate = () => {
  if (!form.customer_name || !form.customer_name.trim()) {
    Taro.showToast({ title: '请输入客户姓名', icon: 'none' })
    return false
  }
  if (!form.gender) {
    Taro.showToast({ title: '请选择性别', icon: 'none' })
    return false
  }
  if (!form.birthday) {
    Taro.showToast({ title: '请选择出生年月日', icon: 'none' })
    return false
  }
  if (form.age === undefined || form.age === '') {
    Taro.showToast({ title: '请填写年龄', icon: 'none' })
    return false
  }
  if (!form.smoker) {
    Taro.showToast({ title: '请选择是否吸烟', icon: 'none' })
    return false
  }
  if (!form.coverage) {
    Taro.showToast({ title: '请输入保额', icon: 'none' })
    return false
  }
  if (!form.payment_period) {
    Taro.showToast({ title: '请选择缴费年期', icon: 'none' })
    return false
  }
  return true
}

/**
 * 清除验证错误
 * @description 由于使用 Toast 显示错误,无需清除状态
 *              保留此方法以保持接口一致性
 */
const clearErrors = () => {
  // 当前使用 Toast 显示错误,无需清除错误状态
  // 如果将来改用内联错误提示,可以在这里清除错误状态
}

defineExpose({
  validate,
  clearErrors
})
</script>

<style lang="less">
/* 模版样式 */
</style>