hookehuyr

refactor(plan): 重构表单提交逻辑,使用模板字段映射

- 重构 PlanFormContainer.vue 的 submit 函数,使用模板配置的 submit_mapping
- 新增完整的储蓄计划书模板 SavingsTemplate.vue
- 在 plan-templates.js 中添加详细的字段映射配置
- 更新 README.md 和 CHANGELOG.md
- 新增 plan-form-schema-usage.md 使用说明文档

影响文件:
- src/components/plan/PlanFormContainer.vue
- src/components/plan/PlanTemplates/SavingsTemplate.vue
- src/config/plan-templates.js
- docs/plan/plan-form-schema-usage.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -7,6 +7,7 @@
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
- **[文档导航](docs/README.md)** - 项目文档索引与使用建议
- **[计划书表单 Schema 使用文档](docs/plan/plan-form-schema-usage.md)** - 计划书表单配置与扩展指南
## 🚀 快速开始
......@@ -50,6 +51,10 @@ pnpm lint
## 🆕 最新更新(2026-02-14)
### 计划书表单演进
-**Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验
-**提交映射下沉** - 提交字段映射从容器迁移到模板配置
### 字段命名优化
-**提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method
-**文档同步** - 更新提取计划相关文档字段示例
......
## [2026-02-14] - 计划书Schema注释与使用文档
### 更新
- 补充计划书Schema与提交映射的详细注释与JSDoc
- 新增计划书Schema使用文档,便于新增保险类型
- README 补充相关文档入口
---
**详细信息**
- **影响文件**: src/config/plan-templates.js, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanFormContainer.vue, docs/plan/plan-form-schema-usage.md, README.md
- **技术栈**: Vue 3, Taro 4
- **测试状态**: 待测试
- **备注**: Schema 配置与提交映射均有详细说明
---
## [2026-02-14] - 计划书表单 Schema 化(方案2)
### 更新
- 储蓄类模板使用表单 Schema 驱动字段渲染与校验
- 提交字段映射迁移到模板配置,统一处理金额转换
- 提取模式切换清空逻辑改为配置驱动
---
**详细信息**
- **影响文件**: src/config/plan-templates.js, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanFormContainer.vue, README.md
- **技术栈**: Vue 3, Taro 4
- **测试状态**: 待测试
- **备注**: 储蓄类产品字段新增仅需调整配置
---
## [2026-02-14] - 文档对齐与业务说明更新
### 更新
......
# 计划书表单 Schema 使用文档
## 1. 文档目标
用于说明计划书表单的 Schema 配置规范、字段类型、联动规则与提交映射,便于后续新增或扩展不同保险类型时快速落地。
## 2. 核心思路
- 统一由 Schema 描述字段渲染、校验与联动
- 统一由 submit_mapping 处理字段到 API 字段的映射与金额转换
- 模板组件只负责“渲染与校验”,不再硬编码字段逻辑
## 3. Schema 结构
```javascript
// Schema 基础结构
const form_schema = {
// 基础字段
base_fields: [
{
id: 'customer_name',
key: 'customer_name',
type: 'name',
label: '申请人',
placeholder: '请输入申请人',
required: true
}
],
// 提取计划字段(可选)
withdrawal_fields: [],
// 联动清空规则(可选)
reset_map: {}
}
```
## 4. 字段类型说明
| type | 组件 | 说明 |
| --- | --- | --- |
| name | NameInput | 姓名输入 |
| radio | RadioGroup | 单选 |
| date | DatePickerGlobal | 日期选择 |
| amount | AmountKeyboard | 金额键盘输入(内部存分) |
| age | AgePickerGlobal | 年龄选择 |
| select | SelectPickerGlobal | 下拉选择 |
| payment_period | PaymentPeriodRadio | 缴费年期 |
| percentage | NutInput | 百分比输入 |
## 5. 字段属性说明
```javascript
// 字段属性示例
{
id: 'coverage',
key: 'coverage',
type: 'amount',
label: '年缴保费',
placeholder: '请输入年缴保费',
input_label: '请输入年缴保费金额',
required: true,
// 可从配置读取币种
currency_from: 'currency',
// 控制显示条件
show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }],
// 默认值
default: '否',
// 标题分组
section_title: '款项提取(允许减少名义金额)'
}
```
## 6. 联动规则与清空逻辑
```javascript
// 提取模式切换后,按规则清空脏字段
const reset_map = {
withdrawal_mode: {
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'],
'指定提取金额': ['withdrawal_start_age', 'withdrawal_period']
}
}
```
## 7. 提交字段映射
```javascript
// submit_mapping 示例(金额字段统一从分转元)
const submit_mapping = {
coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
withdrawal_mode: { api_field: 'withdrawal_option' }
}
```
## 8. 使用示例
```vue
<!-- 储蓄型模板使用示例 -->
<template>
<SavingsTemplate v-model="form_data" :config="template_config" />
</template>
<script setup>
// 表单数据
const form_data = ref({})
// 模板配置(通常来自 plan-templates.js)
const template_config = {
currency: 'USD',
payment_periods: ['整付', '5 年'],
withdrawal_plan: {
enabled: true,
default_currency: 'USD',
withdrawal_periods: ['1年', '2年', '终身']
},
form_schema: {},
submit_mapping: {}
}
</script>
```
## 9. 新增保险类型流程
1.`src/config/plan-templates.js` 新增产品项(配置 form_sn)
2. 为该产品选择已有模板组件或新增模板组件
3. 定义 `form_schema``submit_mapping`
4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑)
5. 验证校验与提交映射
## 10. 新增产品配置示例
```javascript
// 示例:新增储蓄类产品配置
'savings-new': {
name: '示例储蓄产品',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: ['整付', '5 年'],
withdrawal_plan: {
enabled: true,
default_currency: 'USD',
withdrawal_periods: ['1年', '2年', '终身']
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
```
## 11. 常见扩展点
- 新字段:仅在 form_schema 增加字段并补充 submit_mapping
- 新联动:在 show_when 与 reset_map 中定义条件
- 新模板:复用现有字段组件,保持 schema 结构一致
......@@ -237,7 +237,11 @@ const close = async () => {
console.log('[PlanFormContainer] 弹窗已关闭,表单已重置')
}
// 提交表单 - 将表单数据和产品信息提交到后端 API
/**
* 提交表单
* @description 将表单数据与产品信息组装后提交到后端
* @returns {Promise<boolean>} 是否提交成功
*/
const submit = async () => {
if (!props.product) {
console.error('[PlanFormContainer] 无法提交: 产品数据为空')
......@@ -264,24 +268,22 @@ const submit = async () => {
})
try {
// 字段名映射:将表单字段名映射为 API 期望的字段名
// 根据 API 文档 (docs/api-specs/plan/add.md) 定义
const fieldMapping = {
customer_name: 'customer_name', // 申请人(已直接使用)
gender: 'customer_gender', // 性别 → customer_gender
birthday: 'customer_birthday', // 出生年月日 → customer_birthday
smoker: 'smoking_status', // 是否吸烟 → smoking_status
coverage: 'annual_premium', // 保额/年缴保费 → annual_premium
payment_period: 'payment_years', // 缴费年期 → payment_years
withdrawal_enabled: 'allow_reduce_amount', // 是否容许减少名义金额
withdrawal_mode: 'withdrawal_option', // 提取选项
withdrawal_start_age: 'withdrawal_start_age', // 提取开始年龄
withdrawal_period: 'withdrawal_period', // 提取期
currency_type: 'currency_type', // 币种类型
// 新增字段映射
withdrawal_method: 'withdrawal_method', // 提取方式
annual_withdrawal_amount: 'annual_withdrawal_amount', // 每年提取金额
annual_increase_percentage: 'annual_increase_percentage', // 每年递增提取百分比
// 默认字段映射:模板未提供 submit_mapping 时使用
const defaultMapping = {
customer_name: { api_field: 'customer_name' },
gender: { api_field: 'customer_gender' },
birthday: { api_field: 'customer_birthday' },
smoker: { api_field: 'smoking_status' },
coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
payment_period: { api_field: 'payment_years' },
withdrawal_enabled: { api_field: 'allow_reduce_amount' },
withdrawal_mode: { api_field: 'withdrawal_option' },
withdrawal_start_age: { api_field: 'withdrawal_start_age' },
withdrawal_period: { api_field: 'withdrawal_period' },
withdrawal_method: { api_field: 'withdrawal_method' },
annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
annual_increase_percentage: { api_field: 'annual_increase_percentage' },
total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' }
}
// 构建请求数据
......@@ -290,35 +292,19 @@ const submit = async () => {
}
// 映射表单字段到 API 字段
const submitMapping = templateConfig.value?.config?.submit_mapping || defaultMapping
Object.keys(formData.value).forEach(key => {
const apiField = fieldMapping[key]
if (apiField) {
// 有映射:使用映射后的字段名
// 特殊处理:coverage(分)需要转换为元
if (key === 'coverage') {
const coverageInYuan = (formData.value[key] / 100).toFixed(2)
console.log(`[PlanFormContainer] coverage 转换: ${key} (${formData.value[key]} ) ${apiField} (${coverageInYuan} )`)
requestData[apiField] = coverageInYuan
}
// 特殊处理:annual_withdrawal_amount(分)需要转换为元
else if (key === 'annual_withdrawal_amount') {
const amountInYuan = (formData.value[key] / 100).toFixed(2)
console.log(`[PlanFormContainer] annual_withdrawal_amount 转换: ${key} (${formData.value[key]} ) ${apiField} (${amountInYuan} )`)
requestData[apiField] = amountInYuan
}
// 特殊处理:annual_increase_percentage(直接传递,已是字符串)
else if (key === 'annual_increase_percentage') {
requestData[apiField] = formData.value[key]
}
else {
requestData[apiField] = formData.value[key]
const mapping = submitMapping[key]
if (mapping) {
const apiField = typeof mapping === 'string' ? mapping : mapping.api_field
let value = formData.value[key]
// 金额字段从分转换为元
if (typeof mapping === 'object' && mapping.transform === 'fen_to_yuan' && value !== null && value !== undefined && value !== '') {
value = (value / 100).toFixed(2)
}
} else if (key === 'total_amount') {
// 特殊处理:总保费(分 → 元)
requestData.total_premium = (formData.value[key] / 100).toFixed(2)
requestData[apiField] = value
} else {
// 无映射:保持原字段名
requestData[key] = formData.value[key]
}
})
......@@ -329,7 +315,7 @@ const submit = async () => {
}
console.log('[PlanFormContainer] 提交计划书请求数据:', requestData)
console.log('[PlanFormContainer] 字段映射:', fieldMapping)
console.log('[PlanFormContainer] 字段映射:', submitMapping)
// 调用 API
const res = await addAPI(requestData)
......
<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"
/>
<!-- 出生日期 -->
<PlanFieldDatePicker
v-model="form.birthday"
label="出生年月日"
placeholder="请选择年月日"
:required="true"
class="mb-5"
/>
<!-- 是否吸烟 -->
<PlanFieldRadio
v-model="form.smoker"
label="是否吸烟"
:options="['是', '否']"
:required="true"
class="mb-5"
/>
<!-- 保额(年缴保费) -->
<PlanFieldAmount
v-model="form.coverage"
label="年缴保费"
placeholder="请输入年缴保费"
:input-label="'请输入年缴保费金额'"
:currency="config.currency"
:required="true"
class="mb-5"
/>
<!-- 缴费年期 - 单选形式 -->
<PaymentPeriodRadio
v-model="form.payment_period"
label="缴费年期"
:options="config.payment_periods"
:required="true"
class="mb-5"
/>
<!-- 分割线 -->
<div class="border-t border-gray-200 my-6"></div>
<!-- 提取计划配置 -->
<div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section">
<!-- 第一层:是否希望生成一份允许减少名义金额的提取说明? -->
<PlanFieldRadio
v-model="form.withdrawal_enabled"
label="是否希望生成一份允许减少名义金额的提取说明?"
:options="['是', '否']"
:required="true"
<template v-for="field in baseFields" :key="field.id || field.key">
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
</div>
<nut-input
v-model="form[field.key]"
type="digit"
:placeholder="field.placeholder"
@input="(value) => onPercentageInput(value, field.key)"
class="w-full"
/>
</div>
</template>
<!-- 款项提取配置(始终显示,不受上面字段影响) -->
<h3 class="text-base font-semibold text-gray-900 mb-4">款项提取(允许减少名义金额)</h3>
<div class="border-t border-gray-200 my-6"></div>
<!-- 提取选项:指定提取金额 / 最高固定提取金额 -->
<PlanFieldRadio
v-model="form.withdrawal_mode"
label="提取选项"
:options="['指定提取金额', '最高固定提取金额']"
:required="true"
<div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section">
<template v-for="field in withdrawalFields" :key="field.id || field.key">
<h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4">
{{ field.section_title }}
</h3>
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<!-- 指定提取金额模式 -->
<template v-if="form.withdrawal_mode === '指定提取金额'">
<!-- 提取方式:只有按年岁 -->
<PlanFieldRadio
v-model="form.withdrawal_method"
label="提取方式"
:options="['按年岁']"
:required="true"
class="mb-5"
/>
<!-- 按年岁 -->
<template v-if="form.withdrawal_method === '按年岁'">
<!-- 每年提取金额 -->
<PlanFieldAmount
v-model="form.annual_withdrawal_amount"
label="每年提取金额"
placeholder="请输入每年提取金额"
:input-label="'请输入每年提取金额'"
:currency="config.withdrawal_plan.default_currency"
:required="true"
class="mb-5"
/>
<!-- 由几岁开始 -->
<PlanFieldAgePicker
v-model="form.withdrawal_start_age"
label="由几岁开始"
placeholder="请输入开始提取年龄"
:required="true"
class="mb-5"
/>
<!-- 提取期(年) -->
<PlanFieldSelect
v-model="form.withdrawal_period"
label="提取期(年)"
placeholder="请选择提取期"
:options="withdrawalPeriods"
:required="true"
class="mb-5"
/>
<!-- 每年递增提取之百分比 -->
<div class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span class="text-red-500 mr-1">*</span>
<span>每年递增提取之百分比(%</span>
</div>
<nut-input
v-model="form.annual_increase_percentage"
type="digit"
placeholder="请输入递增百分比"
@input="onPercentageInput"
class="w-full"
/>
</div>
</template>
</template>
<!-- 最高固定提取金额模式 -->
<template v-if="form.withdrawal_mode === '最高固定提取金额'">
<!-- 按年岁:由几岁开始 -->
<PlanFieldAgePicker
v-model="form.withdrawal_start_age"
label="按年岁:由几岁开始"
placeholder="请输入开始提取年龄"
:required="true"
class="mb-5"
/>
<!-- 提取期(年) -->
<PlanFieldSelect
v-model="form.withdrawal_period"
label="提取期(年)"
placeholder="请选择提取期"
:options="withdrawalPeriods"
:required="true"
class="mb-5"
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
</div>
<nut-input
v-model="form[field.key]"
type="digit"
:placeholder="field.placeholder"
@input="(value) => onPercentageInput(value, field.key)"
class="w-full"
/>
</template>
</div>
</template>
</div>
</div>
......@@ -187,7 +75,7 @@
* :config="templateConfig"
* />
*/
import { reactive, watch, computed, nextTick } from 'vue'
import { reactive, watch, computed } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldName from '../PlanFields/NameInput.vue'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
......@@ -257,20 +145,121 @@ const form = reactive({})
let previousModelValue = null
// 初始化默认值
// 字段类型与组件的对应关系
const fieldComponentMap = {
name: PlanFieldName,
radio: PlanFieldRadio,
date: PlanFieldDatePicker,
amount: PlanFieldAmount,
age: PlanFieldAgePicker,
select: PlanFieldSelect,
payment_period: PaymentPeriodRadio
}
// Schema 配置入口
const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
const withdrawalFields = computed(() => props.config?.form_schema?.withdrawal_fields || [])
const resetMap = computed(() => props.config?.form_schema?.reset_map || {})
/**
* 获取字段对应的渲染组件
* @param {Object} field - 字段配置
* @returns {Object|null} Vue 组件
*/
const getFieldComponent = (field) => {
return fieldComponentMap[field.type] || null
}
/**
* 组装字段渲染所需的 props
* @param {Object} field - 字段配置
* @returns {Object} 传入字段组件的 props
*/
const getFieldProps = (field) => {
const fieldProps = {
label: field.label,
placeholder: field.placeholder,
required: !!field.required
}
if (field.options) {
fieldProps.options = field.options
}
// 缴费年期选项由模板配置提供
if (field.options_from === 'payment_periods') {
fieldProps.options = fieldProps.options || props.config?.payment_periods
}
// 提取期选项由提取计划配置提供
if (field.options_from === 'withdrawal_plan.withdrawal_periods') {
fieldProps.options = fieldProps.options || props.config?.withdrawal_plan?.withdrawal_periods
}
// 基础币种来自模板配置
if (field.currency_from === 'currency') {
fieldProps.currency = props.config?.currency
}
// 提取计划币种来自提取计划配置
if (field.currency_from === 'withdrawal_plan.default_currency') {
fieldProps.currency = props.config?.withdrawal_plan?.default_currency
}
// 金额键盘的弹窗提示文本
if (field.input_label) {
fieldProps.inputLabel = field.input_label
}
return fieldProps
}
/**
* 判断字段是否可见
* @param {Object} field - 字段配置
* @returns {boolean} 是否显示
*/
const isFieldVisible = (field) => {
if (!field.show_when || field.show_when.length === 0) {
return true
}
return field.show_when.every(condition => {
return form[condition.field] === condition.equals
})
}
/**
* 获取 Schema 默认值
* @param {Object} value - 当前表单数据
* @returns {Object} 默认值集合
*/
const getSchemaDefaults = (value) => {
const defaults = {}
const fields = [...baseFields.value, ...withdrawalFields.value]
fields.forEach(field => {
if (field.default !== undefined && (value?.[field.key] === undefined || value?.[field.key] === null)) {
defaults[field.key] = field.default
}
})
return defaults
}
/**
* 初始化表单数据
* @param {Object} value - 初始数据
*/
const initializeForm = (value) => {
if (!value) {
Object.keys(form).forEach(key => delete form[key])
return
}
const defaults = getSchemaDefaults(value)
Object.assign(form, {
...value,
// 默认值
withdrawal_enabled: value.withdrawal_enabled || '否',
withdrawal_mode: value.withdrawal_mode || '指定提取金额',
withdrawal_method: value.withdrawal_method || '按年岁',
// 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
...defaults,
annual_withdrawal_amount: value.annual_withdrawal_amount ?? null,
annual_increase_percentage: value.annual_increase_percentage ?? null
})
......@@ -298,13 +287,10 @@ watch(
previousModelValue = newVal
} else {
// 正常更新:合并新字段,保留默认值逻辑
const defaults = getSchemaDefaults(newVal)
Object.assign(form, {
...newVal,
// 默认值
withdrawal_enabled: newVal.withdrawal_enabled || '否',
withdrawal_mode: newVal.withdrawal_mode || '指定提取金额',
withdrawal_method: newVal.withdrawal_method || '按年岁',
// 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
...defaults,
annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null,
annual_increase_percentage: newVal.annual_increase_percentage ?? null
})
......@@ -317,6 +303,7 @@ watch(
/**
* 监听表单数据变化,同步到父组件
*/
// 监听提取模式切换,按配置清空脏数据
watch(
() => form,
(newVal) => {
......@@ -330,21 +317,12 @@ watch(
*/
watch(
() => form.withdrawal_mode,
(newMode, oldMode) => {
// 每次切换模式时,只清空输入字段(保留模式选择字段)
if (newMode === '最高固定提取金额') {
// 清空"指定提取金额"模式的输入字段
form.annual_withdrawal_amount = null
form.annual_increase_percentage = null
form.withdrawal_start_age = null
form.withdrawal_period = null
// 立即同步给父组件
emit('update:modelValue', { ...form })
} else if (newMode === '指定提取金额') {
// 清空"最高固定提取金额"模式的输入字段
form.withdrawal_start_age = null
form.withdrawal_period = null
// 立即同步给父组件
(newMode) => {
const resetFields = resetMap.value?.withdrawal_mode?.[newMode] || []
if (resetFields.length > 0) {
resetFields.forEach(key => {
form[key] = null
})
emit('update:modelValue', { ...form })
}
}
......@@ -354,26 +332,18 @@ watch(
* 提取年期选项(从配置读取)
* @type {ComputedRef<Array<string>>}
*/
const withdrawalPeriods = computed(() => {
return props.config?.withdrawal_plan?.withdrawal_periods || [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
})
/**
* 百分比输入限制(实时)
* @description 限制百分比输入为有效数值,最多2位小数
* 只允许输入数字和一个小数点
* @param {string} value - 输入值
*/
const onPercentageInput = (value) => {
/**
* 百分比输入清洗,避免非法字符
* @param {string|number} value - 输入值
* @param {string} key - 目标字段 key
*/
const onPercentageInput = (value, key) => {
// 转换为字符串(处理 value 为 null 或其他类型的情况)
let strValue = String(value ?? '')
......@@ -401,92 +371,37 @@ const onPercentageInput = (value) => {
}
}
// 更新表单值
form.annual_increase_percentage = cleaned
form[key] = cleaned
}
/**
* 表单校验
* @returns {boolean} 是否通过校验
*/
/**
* 表单校验(基于 Schema)
* @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.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
}
const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])]
// 提取计划校验
if (props.config.withdrawal_plan?.enabled) {
// withdrawal_enabled 只是一个可选字段,不需要校验
// 真正需要校验的是提取方案配置
if (!form.withdrawal_mode) {
Taro.showToast({ title: '请选择提取选项', icon: 'none' })
return false
for (const field of fields) {
if (!isFieldVisible(field)) {
continue
}
// 根据选择的提取模式进行校验
if (form.withdrawal_mode === '指定提取金额') {
if (!form.withdrawal_method) {
Taro.showToast({ title: '请选择提取方式', icon: 'none' })
return false
}
if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') {
Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' })
return false
}
if (!form.withdrawal_period) {
Taro.showToast({ title: '请选择提取期', icon: 'none' })
if (field.required) {
const value = form[field.key]
if (value === undefined || value === null || value === '') {
Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' })
return false
}
}
if (form.withdrawal_method === '按年岁') {
if (!form.annual_withdrawal_amount || form.annual_withdrawal_amount === '') {
Taro.showToast({ title: '请输入每年提取金额', icon: 'none' })
return false
}
if (form.annual_increase_percentage === undefined || form.annual_increase_percentage === '') {
Taro.showToast({ title: '请输入每年递增提取之百分比', icon: 'none' })
return false
}
// 验证百分比范围
const percentage = parseFloat(form.annual_increase_percentage)
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
return false
}
}
} else if (form.withdrawal_mode === '最高固定提取金额') {
if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') {
Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' })
return false
}
if (!form.withdrawal_period) {
Taro.showToast({ title: '请选择提取期', icon: 'none' })
if (field.type === 'percentage' && isFieldVisible(field)) {
const percentage = parseFloat(form[field.key])
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
return false
}
}
......
......@@ -19,6 +19,61 @@
* form_sn: "life-insurance-wiop3e" // 对应下面的配置 key
* }
*/
// 基础提交字段映射(适用于人寿/重疾等通用表单)
const baseSubmitMapping = {
customer_name: { api_field: 'customer_name' },
gender: { api_field: 'customer_gender' },
birthday: { api_field: 'customer_birthday' },
smoker: { api_field: 'smoking_status' },
coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
payment_period: { api_field: 'payment_years' },
total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' }
}
// 储蓄类提交字段映射(在基础映射上追加提取计划字段)
const savingsSubmitMapping = {
...baseSubmitMapping,
withdrawal_enabled: { api_field: 'allow_reduce_amount' },
withdrawal_mode: { api_field: 'withdrawal_option' },
withdrawal_start_age: { api_field: 'withdrawal_start_age' },
withdrawal_period: { api_field: 'withdrawal_period' },
withdrawal_method: { api_field: 'withdrawal_method' },
annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
annual_increase_percentage: { api_field: 'annual_increase_percentage' }
}
// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口)
const savingsFormSchema = {
// 基础字段:非提取计划部分
base_fields: [
{ id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true },
{ id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true },
{ id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true },
{ id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true },
{ id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' },
{ id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' }
],
// 提取计划字段:由 withdrawal_plan 开关控制
withdrawal_fields: [
{ id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' },
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' },
{ id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'annual_withdrawal_amount', key: 'annual_withdrawal_amount', type: 'amount', label: '每年提取金额', placeholder: '请输入每年提取金额', input_label: '请输入每年提取金额', required: true, currency_from: 'withdrawal_plan.default_currency', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }, { field: 'withdrawal_method', equals: '按年岁' }] },
{ id: 'withdrawal_start_age_by_year', key: 'withdrawal_start_age', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_period_by_year', key: 'withdrawal_period', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }, { field: 'withdrawal_method', equals: '按年岁' }] },
{ id: 'withdrawal_start_age_by_fixed', key: 'withdrawal_start_age', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
{ id: 'withdrawal_period_by_fixed', key: 'withdrawal_period', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }
],
// 提取模式切换时的清空逻辑,避免脏字段影响提交
reset_map: {
withdrawal_mode: {
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'],
'指定提取金额': ['withdrawal_start_age', 'withdrawal_period']
}
}
}
export const PLAN_TEMPLATES = {
// 人寿保险产品 - WIOP3E
'life-insurance-wiop3e': {
......@@ -33,7 +88,8 @@ export const PLAN_TEMPLATES = {
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 }, // 年龄范围
insurance_period: '终身' // 保险期间
insurance_period: '终身', // 保险期间
submit_mapping: baseSubmitMapping
}
},
......@@ -49,7 +105,8 @@ export const PLAN_TEMPLATES = {
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 },
insurance_period: '终身'
insurance_period: '终身',
submit_mapping: baseSubmitMapping
}
},
......@@ -65,7 +122,8 @@ export const PLAN_TEMPLATES = {
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
insurance_period: '终身',
submit_mapping: baseSubmitMapping
}
},
......@@ -81,7 +139,8 @@ export const PLAN_TEMPLATES = {
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
insurance_period: '终身',
submit_mapping: baseSubmitMapping
}
},
......@@ -97,7 +156,8 @@ export const PLAN_TEMPLATES = {
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
insurance_period: '终身',
submit_mapping: baseSubmitMapping
}
},
......@@ -124,10 +184,7 @@ export const PLAN_TEMPLATES = {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
default_currency: 'USD', // 统一为美元
withdrawal_modes: [
'年龄指定金额', // 方式1
'最高固定金额' // 方式2
],
withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
withdrawal_periods: [
'1年',
'2年',
......@@ -138,7 +195,9 @@ export const PLAN_TEMPLATES = {
'20年',
'终身'
]
}
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
},
......@@ -160,7 +219,7 @@ export const PLAN_TEMPLATES = {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
withdrawal_periods: [
'1年',
'2年',
......@@ -171,7 +230,9 @@ export const PLAN_TEMPLATES = {
'20年',
'终身'
]
}
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
},
......@@ -193,7 +254,7 @@ export const PLAN_TEMPLATES = {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
withdrawal_periods: [
'1年',
'2年',
......@@ -204,7 +265,9 @@ export const PLAN_TEMPLATES = {
'20年',
'终身'
]
}
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
},
......@@ -227,7 +290,7 @@ export const PLAN_TEMPLATES = {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
withdrawal_periods: [
'1年',
'2年',
......@@ -238,7 +301,9 @@ export const PLAN_TEMPLATES = {
'20年',
'终身'
]
}
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
}
......