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>
Showing
6 changed files
with
469 additions
and
319 deletions
| ... | @@ -7,6 +7,7 @@ | ... | @@ -7,6 +7,7 @@ |
| 7 | - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 | 7 | - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 |
| 8 | - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) | 8 | - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) |
| 9 | - **[文档导航](docs/README.md)** - 项目文档索引与使用建议 | 9 | - **[文档导航](docs/README.md)** - 项目文档索引与使用建议 |
| 10 | +- **[计划书表单 Schema 使用文档](docs/plan/plan-form-schema-usage.md)** - 计划书表单配置与扩展指南 | ||
| 10 | 11 | ||
| 11 | ## 🚀 快速开始 | 12 | ## 🚀 快速开始 |
| 12 | 13 | ||
| ... | @@ -50,6 +51,10 @@ pnpm lint | ... | @@ -50,6 +51,10 @@ pnpm lint |
| 50 | 51 | ||
| 51 | ## 🆕 最新更新(2026-02-14) | 52 | ## 🆕 最新更新(2026-02-14) |
| 52 | 53 | ||
| 54 | +### 计划书表单演进 | ||
| 55 | +- ✅ **Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验 | ||
| 56 | +- ✅ **提交映射下沉** - 提交字段映射从容器迁移到模板配置 | ||
| 57 | + | ||
| 53 | ### 字段命名优化 | 58 | ### 字段命名优化 |
| 54 | - ✅ **提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method | 59 | - ✅ **提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method |
| 55 | - ✅ **文档同步** - 更新提取计划相关文档字段示例 | 60 | - ✅ **文档同步** - 更新提取计划相关文档字段示例 | ... | ... |
| 1 | +## [2026-02-14] - 计划书Schema注释与使用文档 | ||
| 2 | + | ||
| 3 | +### 更新 | ||
| 4 | +- 补充计划书Schema与提交映射的详细注释与JSDoc | ||
| 5 | +- 新增计划书Schema使用文档,便于新增保险类型 | ||
| 6 | +- README 补充相关文档入口 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +**详细信息**: | ||
| 11 | +- **影响文件**: 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 | ||
| 12 | +- **技术栈**: Vue 3, Taro 4 | ||
| 13 | +- **测试状态**: 待测试 | ||
| 14 | +- **备注**: Schema 配置与提交映射均有详细说明 | ||
| 15 | + | ||
| 16 | +--- | ||
| 17 | + | ||
| 18 | +## [2026-02-14] - 计划书表单 Schema 化(方案2) | ||
| 19 | + | ||
| 20 | +### 更新 | ||
| 21 | +- 储蓄类模板使用表单 Schema 驱动字段渲染与校验 | ||
| 22 | +- 提交字段映射迁移到模板配置,统一处理金额转换 | ||
| 23 | +- 提取模式切换清空逻辑改为配置驱动 | ||
| 24 | + | ||
| 25 | +--- | ||
| 26 | + | ||
| 27 | +**详细信息**: | ||
| 28 | +- **影响文件**: src/config/plan-templates.js, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanFormContainer.vue, README.md | ||
| 29 | +- **技术栈**: Vue 3, Taro 4 | ||
| 30 | +- **测试状态**: 待测试 | ||
| 31 | +- **备注**: 储蓄类产品字段新增仅需调整配置 | ||
| 32 | + | ||
| 33 | +--- | ||
| 34 | + | ||
| 1 | ## [2026-02-14] - 文档对齐与业务说明更新 | 35 | ## [2026-02-14] - 文档对齐与业务说明更新 |
| 2 | 36 | ||
| 3 | ### 更新 | 37 | ### 更新 | ... | ... |
docs/PLAN/plan-form-schema-usage.md
0 → 100644
| 1 | +# 计划书表单 Schema 使用文档 | ||
| 2 | + | ||
| 3 | +## 1. 文档目标 | ||
| 4 | +用于说明计划书表单的 Schema 配置规范、字段类型、联动规则与提交映射,便于后续新增或扩展不同保险类型时快速落地。 | ||
| 5 | + | ||
| 6 | +## 2. 核心思路 | ||
| 7 | +- 统一由 Schema 描述字段渲染、校验与联动 | ||
| 8 | +- 统一由 submit_mapping 处理字段到 API 字段的映射与金额转换 | ||
| 9 | +- 模板组件只负责“渲染与校验”,不再硬编码字段逻辑 | ||
| 10 | + | ||
| 11 | +## 3. Schema 结构 | ||
| 12 | +```javascript | ||
| 13 | +// Schema 基础结构 | ||
| 14 | +const form_schema = { | ||
| 15 | + // 基础字段 | ||
| 16 | + base_fields: [ | ||
| 17 | + { | ||
| 18 | + id: 'customer_name', | ||
| 19 | + key: 'customer_name', | ||
| 20 | + type: 'name', | ||
| 21 | + label: '申请人', | ||
| 22 | + placeholder: '请输入申请人', | ||
| 23 | + required: true | ||
| 24 | + } | ||
| 25 | + ], | ||
| 26 | + // 提取计划字段(可选) | ||
| 27 | + withdrawal_fields: [], | ||
| 28 | + // 联动清空规则(可选) | ||
| 29 | + reset_map: {} | ||
| 30 | +} | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +## 4. 字段类型说明 | ||
| 34 | +| type | 组件 | 说明 | | ||
| 35 | +| --- | --- | --- | | ||
| 36 | +| name | NameInput | 姓名输入 | | ||
| 37 | +| radio | RadioGroup | 单选 | | ||
| 38 | +| date | DatePickerGlobal | 日期选择 | | ||
| 39 | +| amount | AmountKeyboard | 金额键盘输入(内部存分) | | ||
| 40 | +| age | AgePickerGlobal | 年龄选择 | | ||
| 41 | +| select | SelectPickerGlobal | 下拉选择 | | ||
| 42 | +| payment_period | PaymentPeriodRadio | 缴费年期 | | ||
| 43 | +| percentage | NutInput | 百分比输入 | | ||
| 44 | + | ||
| 45 | +## 5. 字段属性说明 | ||
| 46 | +```javascript | ||
| 47 | +// 字段属性示例 | ||
| 48 | +{ | ||
| 49 | + id: 'coverage', | ||
| 50 | + key: 'coverage', | ||
| 51 | + type: 'amount', | ||
| 52 | + label: '年缴保费', | ||
| 53 | + placeholder: '请输入年缴保费', | ||
| 54 | + input_label: '请输入年缴保费金额', | ||
| 55 | + required: true, | ||
| 56 | + // 可从配置读取币种 | ||
| 57 | + currency_from: 'currency', | ||
| 58 | + // 控制显示条件 | ||
| 59 | + show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }], | ||
| 60 | + // 默认值 | ||
| 61 | + default: '否', | ||
| 62 | + // 标题分组 | ||
| 63 | + section_title: '款项提取(允许减少名义金额)' | ||
| 64 | +} | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +## 6. 联动规则与清空逻辑 | ||
| 68 | +```javascript | ||
| 69 | +// 提取模式切换后,按规则清空脏字段 | ||
| 70 | +const reset_map = { | ||
| 71 | + withdrawal_mode: { | ||
| 72 | + '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'], | ||
| 73 | + '指定提取金额': ['withdrawal_start_age', 'withdrawal_period'] | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | +``` | ||
| 77 | + | ||
| 78 | +## 7. 提交字段映射 | ||
| 79 | +```javascript | ||
| 80 | +// submit_mapping 示例(金额字段统一从分转元) | ||
| 81 | +const submit_mapping = { | ||
| 82 | + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 83 | + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 84 | + withdrawal_mode: { api_field: 'withdrawal_option' } | ||
| 85 | +} | ||
| 86 | +``` | ||
| 87 | + | ||
| 88 | +## 8. 使用示例 | ||
| 89 | +```vue | ||
| 90 | +<!-- 储蓄型模板使用示例 --> | ||
| 91 | +<template> | ||
| 92 | + <SavingsTemplate v-model="form_data" :config="template_config" /> | ||
| 93 | +</template> | ||
| 94 | + | ||
| 95 | +<script setup> | ||
| 96 | +// 表单数据 | ||
| 97 | +const form_data = ref({}) | ||
| 98 | + | ||
| 99 | +// 模板配置(通常来自 plan-templates.js) | ||
| 100 | +const template_config = { | ||
| 101 | + currency: 'USD', | ||
| 102 | + payment_periods: ['整付', '5 年'], | ||
| 103 | + withdrawal_plan: { | ||
| 104 | + enabled: true, | ||
| 105 | + default_currency: 'USD', | ||
| 106 | + withdrawal_periods: ['1年', '2年', '终身'] | ||
| 107 | + }, | ||
| 108 | + form_schema: {}, | ||
| 109 | + submit_mapping: {} | ||
| 110 | +} | ||
| 111 | +</script> | ||
| 112 | +``` | ||
| 113 | + | ||
| 114 | +## 9. 新增保险类型流程 | ||
| 115 | +1. 在 `src/config/plan-templates.js` 新增产品项(配置 form_sn) | ||
| 116 | +2. 为该产品选择已有模板组件或新增模板组件 | ||
| 117 | +3. 定义 `form_schema` 与 `submit_mapping` | ||
| 118 | +4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑) | ||
| 119 | +5. 验证校验与提交映射 | ||
| 120 | + | ||
| 121 | +## 10. 新增产品配置示例 | ||
| 122 | +```javascript | ||
| 123 | +// 示例:新增储蓄类产品配置 | ||
| 124 | +'savings-new': { | ||
| 125 | + name: '示例储蓄产品', | ||
| 126 | + component: 'SavingsTemplate', | ||
| 127 | + category: 'savings', | ||
| 128 | + config: { | ||
| 129 | + currency: 'USD', | ||
| 130 | + payment_periods: ['整付', '5 年'], | ||
| 131 | + withdrawal_plan: { | ||
| 132 | + enabled: true, | ||
| 133 | + default_currency: 'USD', | ||
| 134 | + withdrawal_periods: ['1年', '2年', '终身'] | ||
| 135 | + }, | ||
| 136 | + form_schema: savingsFormSchema, | ||
| 137 | + submit_mapping: savingsSubmitMapping | ||
| 138 | + } | ||
| 139 | +} | ||
| 140 | +``` | ||
| 141 | + | ||
| 142 | +## 11. 常见扩展点 | ||
| 143 | +- 新字段:仅在 form_schema 增加字段并补充 submit_mapping | ||
| 144 | +- 新联动:在 show_when 与 reset_map 中定义条件 | ||
| 145 | +- 新模板:复用现有字段组件,保持 schema 结构一致 |
| ... | @@ -237,7 +237,11 @@ const close = async () => { | ... | @@ -237,7 +237,11 @@ const close = async () => { |
| 237 | console.log('[PlanFormContainer] 弹窗已关闭,表单已重置') | 237 | console.log('[PlanFormContainer] 弹窗已关闭,表单已重置') |
| 238 | } | 238 | } |
| 239 | 239 | ||
| 240 | -// 提交表单 - 将表单数据和产品信息提交到后端 API | 240 | +/** |
| 241 | + * 提交表单 | ||
| 242 | + * @description 将表单数据与产品信息组装后提交到后端 | ||
| 243 | + * @returns {Promise<boolean>} 是否提交成功 | ||
| 244 | + */ | ||
| 241 | const submit = async () => { | 245 | const submit = async () => { |
| 242 | if (!props.product) { | 246 | if (!props.product) { |
| 243 | console.error('[PlanFormContainer] 无法提交: 产品数据为空') | 247 | console.error('[PlanFormContainer] 无法提交: 产品数据为空') |
| ... | @@ -264,24 +268,22 @@ const submit = async () => { | ... | @@ -264,24 +268,22 @@ const submit = async () => { |
| 264 | }) | 268 | }) |
| 265 | 269 | ||
| 266 | try { | 270 | try { |
| 267 | - // 字段名映射:将表单字段名映射为 API 期望的字段名 | 271 | + // 默认字段映射:模板未提供 submit_mapping 时使用 |
| 268 | - // 根据 API 文档 (docs/api-specs/plan/add.md) 定义 | 272 | + const defaultMapping = { |
| 269 | - const fieldMapping = { | 273 | + customer_name: { api_field: 'customer_name' }, |
| 270 | - customer_name: 'customer_name', // 申请人(已直接使用) | 274 | + gender: { api_field: 'customer_gender' }, |
| 271 | - gender: 'customer_gender', // 性别 → customer_gender | 275 | + birthday: { api_field: 'customer_birthday' }, |
| 272 | - birthday: 'customer_birthday', // 出生年月日 → customer_birthday | 276 | + smoker: { api_field: 'smoking_status' }, |
| 273 | - smoker: 'smoking_status', // 是否吸烟 → smoking_status | 277 | + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, |
| 274 | - coverage: 'annual_premium', // 保额/年缴保费 → annual_premium | 278 | + payment_period: { api_field: 'payment_years' }, |
| 275 | - payment_period: 'payment_years', // 缴费年期 → payment_years | 279 | + withdrawal_enabled: { api_field: 'allow_reduce_amount' }, |
| 276 | - withdrawal_enabled: 'allow_reduce_amount', // 是否容许减少名义金额 | 280 | + withdrawal_mode: { api_field: 'withdrawal_option' }, |
| 277 | - withdrawal_mode: 'withdrawal_option', // 提取选项 | 281 | + withdrawal_start_age: { api_field: 'withdrawal_start_age' }, |
| 278 | - withdrawal_start_age: 'withdrawal_start_age', // 提取开始年龄 | 282 | + withdrawal_period: { api_field: 'withdrawal_period' }, |
| 279 | - withdrawal_period: 'withdrawal_period', // 提取期 | 283 | + withdrawal_method: { api_field: 'withdrawal_method' }, |
| 280 | - currency_type: 'currency_type', // 币种类型 | 284 | + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, |
| 281 | - // 新增字段映射 | 285 | + annual_increase_percentage: { api_field: 'annual_increase_percentage' }, |
| 282 | - withdrawal_method: 'withdrawal_method', // 提取方式 | 286 | + total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } |
| 283 | - annual_withdrawal_amount: 'annual_withdrawal_amount', // 每年提取金额 | ||
| 284 | - annual_increase_percentage: 'annual_increase_percentage', // 每年递增提取百分比 | ||
| 285 | } | 287 | } |
| 286 | 288 | ||
| 287 | // 构建请求数据 | 289 | // 构建请求数据 |
| ... | @@ -290,35 +292,19 @@ const submit = async () => { | ... | @@ -290,35 +292,19 @@ const submit = async () => { |
| 290 | } | 292 | } |
| 291 | 293 | ||
| 292 | // 映射表单字段到 API 字段 | 294 | // 映射表单字段到 API 字段 |
| 295 | + const submitMapping = templateConfig.value?.config?.submit_mapping || defaultMapping | ||
| 296 | + | ||
| 293 | Object.keys(formData.value).forEach(key => { | 297 | Object.keys(formData.value).forEach(key => { |
| 294 | - const apiField = fieldMapping[key] | 298 | + const mapping = submitMapping[key] |
| 295 | - | 299 | + if (mapping) { |
| 296 | - if (apiField) { | 300 | + const apiField = typeof mapping === 'string' ? mapping : mapping.api_field |
| 297 | - // 有映射:使用映射后的字段名 | 301 | + let value = formData.value[key] |
| 298 | - // 特殊处理:coverage(分)需要转换为元 | 302 | + // 金额字段从分转换为元 |
| 299 | - if (key === 'coverage') { | 303 | + if (typeof mapping === 'object' && mapping.transform === 'fen_to_yuan' && value !== null && value !== undefined && value !== '') { |
| 300 | - const coverageInYuan = (formData.value[key] / 100).toFixed(2) | 304 | + value = (value / 100).toFixed(2) |
| 301 | - console.log(`[PlanFormContainer] coverage 转换: ${key} (${formData.value[key]} 分) → ${apiField} (${coverageInYuan} 元)`) | ||
| 302 | - requestData[apiField] = coverageInYuan | ||
| 303 | - } | ||
| 304 | - // 特殊处理:annual_withdrawal_amount(分)需要转换为元 | ||
| 305 | - else if (key === 'annual_withdrawal_amount') { | ||
| 306 | - const amountInYuan = (formData.value[key] / 100).toFixed(2) | ||
| 307 | - console.log(`[PlanFormContainer] annual_withdrawal_amount 转换: ${key} (${formData.value[key]} 分) → ${apiField} (${amountInYuan} 元)`) | ||
| 308 | - requestData[apiField] = amountInYuan | ||
| 309 | - } | ||
| 310 | - // 特殊处理:annual_increase_percentage(直接传递,已是字符串) | ||
| 311 | - else if (key === 'annual_increase_percentage') { | ||
| 312 | - requestData[apiField] = formData.value[key] | ||
| 313 | - } | ||
| 314 | - else { | ||
| 315 | - requestData[apiField] = formData.value[key] | ||
| 316 | } | 305 | } |
| 317 | - } else if (key === 'total_amount') { | 306 | + requestData[apiField] = value |
| 318 | - // 特殊处理:总保费(分 → 元) | ||
| 319 | - requestData.total_premium = (formData.value[key] / 100).toFixed(2) | ||
| 320 | } else { | 307 | } else { |
| 321 | - // 无映射:保持原字段名 | ||
| 322 | requestData[key] = formData.value[key] | 308 | requestData[key] = formData.value[key] |
| 323 | } | 309 | } |
| 324 | }) | 310 | }) |
| ... | @@ -329,7 +315,7 @@ const submit = async () => { | ... | @@ -329,7 +315,7 @@ const submit = async () => { |
| 329 | } | 315 | } |
| 330 | 316 | ||
| 331 | console.log('[PlanFormContainer] 提交计划书请求数据:', requestData) | 317 | console.log('[PlanFormContainer] 提交计划书请求数据:', requestData) |
| 332 | - console.log('[PlanFormContainer] 字段映射:', fieldMapping) | 318 | + console.log('[PlanFormContainer] 字段映射:', submitMapping) |
| 333 | 319 | ||
| 334 | // 调用 API | 320 | // 调用 API |
| 335 | const res = await addAPI(requestData) | 321 | const res = await addAPI(requestData) | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <div v-if="config"> | 2 | <div v-if="config"> |
| 3 | - <!-- 申请人 --> | 3 | + <template v-for="field in baseFields" :key="field.id || field.key"> |
| 4 | - <PlanFieldName | 4 | + <component |
| 5 | - v-model="form.customer_name" | 5 | + v-if="isFieldVisible(field) && field.type !== 'percentage'" |
| 6 | - label="申请人" | 6 | + :is="getFieldComponent(field)" |
| 7 | - placeholder="请输入申请人" | 7 | + v-model="form[field.key]" |
| 8 | - :required="true" | 8 | + v-bind="getFieldProps(field)" |
| 9 | class="mb-5" | 9 | class="mb-5" |
| 10 | /> | 10 | /> |
| 11 | - | 11 | + <div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5"> |
| 12 | - <!-- 性别 --> | 12 | + <div class="text-sm text-gray-700 mb-2 flex items-center"> |
| 13 | - <PlanFieldRadio | 13 | + <span v-if="field.required" class="text-red-500 mr-1">*</span> |
| 14 | - v-model="form.gender" | 14 | + <span>{{ field.label }}</span> |
| 15 | - label="性别" | 15 | + </div> |
| 16 | - :options="['男', '女']" | 16 | + <nut-input |
| 17 | - :required="true" | 17 | + v-model="form[field.key]" |
| 18 | - class="mb-5" | 18 | + type="digit" |
| 19 | - /> | 19 | + :placeholder="field.placeholder" |
| 20 | - | 20 | + @input="(value) => onPercentageInput(value, field.key)" |
| 21 | - <!-- 出生日期 --> | 21 | + class="w-full" |
| 22 | - <PlanFieldDatePicker | ||
| 23 | - v-model="form.birthday" | ||
| 24 | - label="出生年月日" | ||
| 25 | - placeholder="请选择年月日" | ||
| 26 | - :required="true" | ||
| 27 | - class="mb-5" | ||
| 28 | - /> | ||
| 29 | - | ||
| 30 | - <!-- 是否吸烟 --> | ||
| 31 | - <PlanFieldRadio | ||
| 32 | - v-model="form.smoker" | ||
| 33 | - label="是否吸烟" | ||
| 34 | - :options="['是', '否']" | ||
| 35 | - :required="true" | ||
| 36 | - class="mb-5" | ||
| 37 | - /> | ||
| 38 | - | ||
| 39 | - <!-- 保额(年缴保费) --> | ||
| 40 | - <PlanFieldAmount | ||
| 41 | - v-model="form.coverage" | ||
| 42 | - label="年缴保费" | ||
| 43 | - placeholder="请输入年缴保费" | ||
| 44 | - :input-label="'请输入年缴保费金额'" | ||
| 45 | - :currency="config.currency" | ||
| 46 | - :required="true" | ||
| 47 | - class="mb-5" | ||
| 48 | - /> | ||
| 49 | - | ||
| 50 | - <!-- 缴费年期 - 单选形式 --> | ||
| 51 | - <PaymentPeriodRadio | ||
| 52 | - v-model="form.payment_period" | ||
| 53 | - label="缴费年期" | ||
| 54 | - :options="config.payment_periods" | ||
| 55 | - :required="true" | ||
| 56 | - class="mb-5" | ||
| 57 | /> | 22 | /> |
| 23 | + </div> | ||
| 24 | + </template> | ||
| 58 | 25 | ||
| 59 | - <!-- 分割线 --> | ||
| 60 | <div class="border-t border-gray-200 my-6"></div> | 26 | <div class="border-t border-gray-200 my-6"></div> |
| 61 | 27 | ||
| 62 | - <!-- 提取计划配置 --> | ||
| 63 | <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> | 28 | <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> |
| 64 | - <!-- 第一层:是否希望生成一份允许减少名义金额的提取说明? --> | 29 | + <template v-for="field in withdrawalFields" :key="field.id || field.key"> |
| 65 | - <PlanFieldRadio | 30 | + <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4"> |
| 66 | - v-model="form.withdrawal_enabled" | 31 | + {{ field.section_title }} |
| 67 | - label="是否希望生成一份允许减少名义金额的提取说明?" | 32 | + </h3> |
| 68 | - :options="['是', '否']" | 33 | + <component |
| 69 | - :required="true" | 34 | + v-if="isFieldVisible(field) && field.type !== 'percentage'" |
| 70 | - class="mb-5" | 35 | + :is="getFieldComponent(field)" |
| 71 | - /> | 36 | + v-model="form[field.key]" |
| 72 | - | 37 | + v-bind="getFieldProps(field)" |
| 73 | - <!-- 款项提取配置(始终显示,不受上面字段影响) --> | ||
| 74 | - <h3 class="text-base font-semibold text-gray-900 mb-4">款项提取(允许减少名义金额)</h3> | ||
| 75 | - | ||
| 76 | - <!-- 提取选项:指定提取金额 / 最高固定提取金额 --> | ||
| 77 | - <PlanFieldRadio | ||
| 78 | - v-model="form.withdrawal_mode" | ||
| 79 | - label="提取选项" | ||
| 80 | - :options="['指定提取金额', '最高固定提取金额']" | ||
| 81 | - :required="true" | ||
| 82 | - class="mb-5" | ||
| 83 | - /> | ||
| 84 | - | ||
| 85 | - <!-- 指定提取金额模式 --> | ||
| 86 | - <template v-if="form.withdrawal_mode === '指定提取金额'"> | ||
| 87 | - <!-- 提取方式:只有按年岁 --> | ||
| 88 | - <PlanFieldRadio | ||
| 89 | - v-model="form.withdrawal_method" | ||
| 90 | - label="提取方式" | ||
| 91 | - :options="['按年岁']" | ||
| 92 | - :required="true" | ||
| 93 | class="mb-5" | 38 | class="mb-5" |
| 94 | /> | 39 | /> |
| 95 | - | 40 | + <div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5"> |
| 96 | - <!-- 按年岁 --> | ||
| 97 | - <template v-if="form.withdrawal_method === '按年岁'"> | ||
| 98 | - <!-- 每年提取金额 --> | ||
| 99 | - <PlanFieldAmount | ||
| 100 | - v-model="form.annual_withdrawal_amount" | ||
| 101 | - label="每年提取金额" | ||
| 102 | - placeholder="请输入每年提取金额" | ||
| 103 | - :input-label="'请输入每年提取金额'" | ||
| 104 | - :currency="config.withdrawal_plan.default_currency" | ||
| 105 | - :required="true" | ||
| 106 | - class="mb-5" | ||
| 107 | - /> | ||
| 108 | - | ||
| 109 | - <!-- 由几岁开始 --> | ||
| 110 | - <PlanFieldAgePicker | ||
| 111 | - v-model="form.withdrawal_start_age" | ||
| 112 | - label="由几岁开始" | ||
| 113 | - placeholder="请输入开始提取年龄" | ||
| 114 | - :required="true" | ||
| 115 | - class="mb-5" | ||
| 116 | - /> | ||
| 117 | - | ||
| 118 | - <!-- 提取期(年) --> | ||
| 119 | - <PlanFieldSelect | ||
| 120 | - v-model="form.withdrawal_period" | ||
| 121 | - label="提取期(年)" | ||
| 122 | - placeholder="请选择提取期" | ||
| 123 | - :options="withdrawalPeriods" | ||
| 124 | - :required="true" | ||
| 125 | - class="mb-5" | ||
| 126 | - /> | ||
| 127 | - | ||
| 128 | - <!-- 每年递增提取之百分比 --> | ||
| 129 | - <div class="mb-5"> | ||
| 130 | <div class="text-sm text-gray-700 mb-2 flex items-center"> | 41 | <div class="text-sm text-gray-700 mb-2 flex items-center"> |
| 131 | - <span class="text-red-500 mr-1">*</span> | 42 | + <span v-if="field.required" class="text-red-500 mr-1">*</span> |
| 132 | - <span>每年递增提取之百分比(%)</span> | 43 | + <span>{{ field.label }}</span> |
| 133 | </div> | 44 | </div> |
| 134 | <nut-input | 45 | <nut-input |
| 135 | - v-model="form.annual_increase_percentage" | 46 | + v-model="form[field.key]" |
| 136 | type="digit" | 47 | type="digit" |
| 137 | - placeholder="请输入递增百分比" | 48 | + :placeholder="field.placeholder" |
| 138 | - @input="onPercentageInput" | 49 | + @input="(value) => onPercentageInput(value, field.key)" |
| 139 | class="w-full" | 50 | class="w-full" |
| 140 | /> | 51 | /> |
| 141 | </div> | 52 | </div> |
| 142 | </template> | 53 | </template> |
| 143 | - </template> | ||
| 144 | - | ||
| 145 | - <!-- 最高固定提取金额模式 --> | ||
| 146 | - <template v-if="form.withdrawal_mode === '最高固定提取金额'"> | ||
| 147 | - <!-- 按年岁:由几岁开始 --> | ||
| 148 | - <PlanFieldAgePicker | ||
| 149 | - v-model="form.withdrawal_start_age" | ||
| 150 | - label="按年岁:由几岁开始" | ||
| 151 | - placeholder="请输入开始提取年龄" | ||
| 152 | - :required="true" | ||
| 153 | - class="mb-5" | ||
| 154 | - /> | ||
| 155 | - | ||
| 156 | - <!-- 提取期(年) --> | ||
| 157 | - <PlanFieldSelect | ||
| 158 | - v-model="form.withdrawal_period" | ||
| 159 | - label="提取期(年)" | ||
| 160 | - placeholder="请选择提取期" | ||
| 161 | - :options="withdrawalPeriods" | ||
| 162 | - :required="true" | ||
| 163 | - class="mb-5" | ||
| 164 | - /> | ||
| 165 | - </template> | ||
| 166 | </div> | 54 | </div> |
| 167 | </div> | 55 | </div> |
| 168 | 56 | ||
| ... | @@ -187,7 +75,7 @@ | ... | @@ -187,7 +75,7 @@ |
| 187 | * :config="templateConfig" | 75 | * :config="templateConfig" |
| 188 | * /> | 76 | * /> |
| 189 | */ | 77 | */ |
| 190 | -import { reactive, watch, computed, nextTick } from 'vue' | 78 | +import { reactive, watch, computed } from 'vue' |
| 191 | import Taro from '@tarojs/taro' | 79 | import Taro from '@tarojs/taro' |
| 192 | import PlanFieldName from '../PlanFields/NameInput.vue' | 80 | import PlanFieldName from '../PlanFields/NameInput.vue' |
| 193 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | 81 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' |
| ... | @@ -257,20 +145,121 @@ const form = reactive({}) | ... | @@ -257,20 +145,121 @@ const form = reactive({}) |
| 257 | 145 | ||
| 258 | let previousModelValue = null | 146 | let previousModelValue = null |
| 259 | 147 | ||
| 260 | -// 初始化默认值 | 148 | +// 字段类型与组件的对应关系 |
| 149 | +const fieldComponentMap = { | ||
| 150 | + name: PlanFieldName, | ||
| 151 | + radio: PlanFieldRadio, | ||
| 152 | + date: PlanFieldDatePicker, | ||
| 153 | + amount: PlanFieldAmount, | ||
| 154 | + age: PlanFieldAgePicker, | ||
| 155 | + select: PlanFieldSelect, | ||
| 156 | + payment_period: PaymentPeriodRadio | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +// Schema 配置入口 | ||
| 160 | +const baseFields = computed(() => props.config?.form_schema?.base_fields || []) | ||
| 161 | +const withdrawalFields = computed(() => props.config?.form_schema?.withdrawal_fields || []) | ||
| 162 | +const resetMap = computed(() => props.config?.form_schema?.reset_map || {}) | ||
| 163 | + | ||
| 164 | +/** | ||
| 165 | + * 获取字段对应的渲染组件 | ||
| 166 | + * @param {Object} field - 字段配置 | ||
| 167 | + * @returns {Object|null} Vue 组件 | ||
| 168 | + */ | ||
| 169 | +const getFieldComponent = (field) => { | ||
| 170 | + return fieldComponentMap[field.type] || null | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +/** | ||
| 174 | + * 组装字段渲染所需的 props | ||
| 175 | + * @param {Object} field - 字段配置 | ||
| 176 | + * @returns {Object} 传入字段组件的 props | ||
| 177 | + */ | ||
| 178 | +const getFieldProps = (field) => { | ||
| 179 | + const fieldProps = { | ||
| 180 | + label: field.label, | ||
| 181 | + placeholder: field.placeholder, | ||
| 182 | + required: !!field.required | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + if (field.options) { | ||
| 186 | + fieldProps.options = field.options | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + // 缴费年期选项由模板配置提供 | ||
| 190 | + if (field.options_from === 'payment_periods') { | ||
| 191 | + fieldProps.options = fieldProps.options || props.config?.payment_periods | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + // 提取期选项由提取计划配置提供 | ||
| 195 | + if (field.options_from === 'withdrawal_plan.withdrawal_periods') { | ||
| 196 | + fieldProps.options = fieldProps.options || props.config?.withdrawal_plan?.withdrawal_periods | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + // 基础币种来自模板配置 | ||
| 200 | + if (field.currency_from === 'currency') { | ||
| 201 | + fieldProps.currency = props.config?.currency | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + // 提取计划币种来自提取计划配置 | ||
| 205 | + if (field.currency_from === 'withdrawal_plan.default_currency') { | ||
| 206 | + fieldProps.currency = props.config?.withdrawal_plan?.default_currency | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + // 金额键盘的弹窗提示文本 | ||
| 210 | + if (field.input_label) { | ||
| 211 | + fieldProps.inputLabel = field.input_label | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + return fieldProps | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +/** | ||
| 218 | + * 判断字段是否可见 | ||
| 219 | + * @param {Object} field - 字段配置 | ||
| 220 | + * @returns {boolean} 是否显示 | ||
| 221 | + */ | ||
| 222 | +const isFieldVisible = (field) => { | ||
| 223 | + if (!field.show_when || field.show_when.length === 0) { | ||
| 224 | + return true | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + return field.show_when.every(condition => { | ||
| 228 | + return form[condition.field] === condition.equals | ||
| 229 | + }) | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +/** | ||
| 233 | + * 获取 Schema 默认值 | ||
| 234 | + * @param {Object} value - 当前表单数据 | ||
| 235 | + * @returns {Object} 默认值集合 | ||
| 236 | + */ | ||
| 237 | +const getSchemaDefaults = (value) => { | ||
| 238 | + const defaults = {} | ||
| 239 | + const fields = [...baseFields.value, ...withdrawalFields.value] | ||
| 240 | + fields.forEach(field => { | ||
| 241 | + if (field.default !== undefined && (value?.[field.key] === undefined || value?.[field.key] === null)) { | ||
| 242 | + defaults[field.key] = field.default | ||
| 243 | + } | ||
| 244 | + }) | ||
| 245 | + return defaults | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +/** | ||
| 249 | + * 初始化表单数据 | ||
| 250 | + * @param {Object} value - 初始数据 | ||
| 251 | + */ | ||
| 261 | const initializeForm = (value) => { | 252 | const initializeForm = (value) => { |
| 262 | if (!value) { | 253 | if (!value) { |
| 263 | Object.keys(form).forEach(key => delete form[key]) | 254 | Object.keys(form).forEach(key => delete form[key]) |
| 264 | return | 255 | return |
| 265 | } | 256 | } |
| 266 | 257 | ||
| 258 | + const defaults = getSchemaDefaults(value) | ||
| 259 | + | ||
| 267 | Object.assign(form, { | 260 | Object.assign(form, { |
| 268 | ...value, | 261 | ...value, |
| 269 | - // 默认值 | 262 | + ...defaults, |
| 270 | - withdrawal_enabled: value.withdrawal_enabled || '否', | ||
| 271 | - withdrawal_mode: value.withdrawal_mode || '指定提取金额', | ||
| 272 | - withdrawal_method: value.withdrawal_method || '按年岁', | ||
| 273 | - // 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型) | ||
| 274 | annual_withdrawal_amount: value.annual_withdrawal_amount ?? null, | 263 | annual_withdrawal_amount: value.annual_withdrawal_amount ?? null, |
| 275 | annual_increase_percentage: value.annual_increase_percentage ?? null | 264 | annual_increase_percentage: value.annual_increase_percentage ?? null |
| 276 | }) | 265 | }) |
| ... | @@ -298,13 +287,10 @@ watch( | ... | @@ -298,13 +287,10 @@ watch( |
| 298 | previousModelValue = newVal | 287 | previousModelValue = newVal |
| 299 | } else { | 288 | } else { |
| 300 | // 正常更新:合并新字段,保留默认值逻辑 | 289 | // 正常更新:合并新字段,保留默认值逻辑 |
| 290 | + const defaults = getSchemaDefaults(newVal) | ||
| 301 | Object.assign(form, { | 291 | Object.assign(form, { |
| 302 | ...newVal, | 292 | ...newVal, |
| 303 | - // 默认值 | 293 | + ...defaults, |
| 304 | - withdrawal_enabled: newVal.withdrawal_enabled || '否', | ||
| 305 | - withdrawal_mode: newVal.withdrawal_mode || '指定提取金额', | ||
| 306 | - withdrawal_method: newVal.withdrawal_method || '按年岁', | ||
| 307 | - // 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型) | ||
| 308 | annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null, | 294 | annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null, |
| 309 | annual_increase_percentage: newVal.annual_increase_percentage ?? null | 295 | annual_increase_percentage: newVal.annual_increase_percentage ?? null |
| 310 | }) | 296 | }) |
| ... | @@ -317,6 +303,7 @@ watch( | ... | @@ -317,6 +303,7 @@ watch( |
| 317 | /** | 303 | /** |
| 318 | * 监听表单数据变化,同步到父组件 | 304 | * 监听表单数据变化,同步到父组件 |
| 319 | */ | 305 | */ |
| 306 | +// 监听提取模式切换,按配置清空脏数据 | ||
| 320 | watch( | 307 | watch( |
| 321 | () => form, | 308 | () => form, |
| 322 | (newVal) => { | 309 | (newVal) => { |
| ... | @@ -330,21 +317,12 @@ watch( | ... | @@ -330,21 +317,12 @@ watch( |
| 330 | */ | 317 | */ |
| 331 | watch( | 318 | watch( |
| 332 | () => form.withdrawal_mode, | 319 | () => form.withdrawal_mode, |
| 333 | - (newMode, oldMode) => { | 320 | + (newMode) => { |
| 334 | - // 每次切换模式时,只清空输入字段(保留模式选择字段) | 321 | + const resetFields = resetMap.value?.withdrawal_mode?.[newMode] || [] |
| 335 | - if (newMode === '最高固定提取金额') { | 322 | + if (resetFields.length > 0) { |
| 336 | - // 清空"指定提取金额"模式的输入字段 | 323 | + resetFields.forEach(key => { |
| 337 | - form.annual_withdrawal_amount = null | 324 | + form[key] = null |
| 338 | - form.annual_increase_percentage = null | 325 | + }) |
| 339 | - form.withdrawal_start_age = null | ||
| 340 | - form.withdrawal_period = null | ||
| 341 | - // 立即同步给父组件 | ||
| 342 | - emit('update:modelValue', { ...form }) | ||
| 343 | - } else if (newMode === '指定提取金额') { | ||
| 344 | - // 清空"最高固定提取金额"模式的输入字段 | ||
| 345 | - form.withdrawal_start_age = null | ||
| 346 | - form.withdrawal_period = null | ||
| 347 | - // 立即同步给父组件 | ||
| 348 | emit('update:modelValue', { ...form }) | 326 | emit('update:modelValue', { ...form }) |
| 349 | } | 327 | } |
| 350 | } | 328 | } |
| ... | @@ -354,26 +332,18 @@ watch( | ... | @@ -354,26 +332,18 @@ watch( |
| 354 | * 提取年期选项(从配置读取) | 332 | * 提取年期选项(从配置读取) |
| 355 | * @type {ComputedRef<Array<string>>} | 333 | * @type {ComputedRef<Array<string>>} |
| 356 | */ | 334 | */ |
| 357 | -const withdrawalPeriods = computed(() => { | ||
| 358 | - return props.config?.withdrawal_plan?.withdrawal_periods || [ | ||
| 359 | - '1年', | ||
| 360 | - '2年', | ||
| 361 | - '3年', | ||
| 362 | - '5年', | ||
| 363 | - '10年', | ||
| 364 | - '15年', | ||
| 365 | - '20年', | ||
| 366 | - '终身' | ||
| 367 | - ] | ||
| 368 | -}) | ||
| 369 | - | ||
| 370 | /** | 335 | /** |
| 371 | * 百分比输入限制(实时) | 336 | * 百分比输入限制(实时) |
| 372 | * @description 限制百分比输入为有效数值,最多2位小数 | 337 | * @description 限制百分比输入为有效数值,最多2位小数 |
| 373 | * 只允许输入数字和一个小数点 | 338 | * 只允许输入数字和一个小数点 |
| 374 | * @param {string} value - 输入值 | 339 | * @param {string} value - 输入值 |
| 375 | */ | 340 | */ |
| 376 | -const onPercentageInput = (value) => { | 341 | +/** |
| 342 | + * 百分比输入清洗,避免非法字符 | ||
| 343 | + * @param {string|number} value - 输入值 | ||
| 344 | + * @param {string} key - 目标字段 key | ||
| 345 | + */ | ||
| 346 | +const onPercentageInput = (value, key) => { | ||
| 377 | // 转换为字符串(处理 value 为 null 或其他类型的情况) | 347 | // 转换为字符串(处理 value 为 null 或其他类型的情况) |
| 378 | let strValue = String(value ?? '') | 348 | let strValue = String(value ?? '') |
| 379 | 349 | ||
| ... | @@ -401,95 +371,40 @@ const onPercentageInput = (value) => { | ... | @@ -401,95 +371,40 @@ const onPercentageInput = (value) => { |
| 401 | } | 371 | } |
| 402 | } | 372 | } |
| 403 | 373 | ||
| 404 | - // 更新表单值 | 374 | + form[key] = cleaned |
| 405 | - form.annual_increase_percentage = cleaned | ||
| 406 | } | 375 | } |
| 407 | 376 | ||
| 408 | /** | 377 | /** |
| 409 | * 表单校验 | 378 | * 表单校验 |
| 410 | * @returns {boolean} 是否通过校验 | 379 | * @returns {boolean} 是否通过校验 |
| 411 | */ | 380 | */ |
| 381 | +/** | ||
| 382 | + * 表单校验(基于 Schema) | ||
| 383 | + * @returns {boolean} 校验是否通过 | ||
| 384 | + */ | ||
| 412 | const validate = () => { | 385 | const validate = () => { |
| 413 | - // 基础字段校验 | 386 | + const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] |
| 414 | - if (!form.customer_name || !form.customer_name.trim()) { | ||
| 415 | - Taro.showToast({ title: '请输入申请人', icon: 'none' }) | ||
| 416 | - return false | ||
| 417 | - } | ||
| 418 | - if (!form.gender) { | ||
| 419 | - Taro.showToast({ title: '请选择性别', icon: 'none' }) | ||
| 420 | - return false | ||
| 421 | - } | ||
| 422 | - if (!form.birthday) { | ||
| 423 | - Taro.showToast({ title: '请选择出生年月日', icon: 'none' }) | ||
| 424 | - return false | ||
| 425 | - } | ||
| 426 | - if (!form.smoker) { | ||
| 427 | - Taro.showToast({ title: '请选择是否吸烟', icon: 'none' }) | ||
| 428 | - return false | ||
| 429 | - } | ||
| 430 | - if (!form.coverage) { | ||
| 431 | - Taro.showToast({ title: '请输入年缴保费', icon: 'none' }) | ||
| 432 | - return false | ||
| 433 | - } | ||
| 434 | - if (!form.payment_period) { | ||
| 435 | - Taro.showToast({ title: '请选择缴费年期', icon: 'none' }) | ||
| 436 | - return false | ||
| 437 | - } | ||
| 438 | 387 | ||
| 439 | - // 提取计划校验 | 388 | + for (const field of fields) { |
| 440 | - if (props.config.withdrawal_plan?.enabled) { | 389 | + if (!isFieldVisible(field)) { |
| 441 | - // withdrawal_enabled 只是一个可选字段,不需要校验 | 390 | + continue |
| 442 | - // 真正需要校验的是提取方案配置 | ||
| 443 | - | ||
| 444 | - if (!form.withdrawal_mode) { | ||
| 445 | - Taro.showToast({ title: '请选择提取选项', icon: 'none' }) | ||
| 446 | - return false | ||
| 447 | } | 391 | } |
| 448 | 392 | ||
| 449 | - // 根据选择的提取模式进行校验 | 393 | + if (field.required) { |
| 450 | - if (form.withdrawal_mode === '指定提取金额') { | 394 | + const value = form[field.key] |
| 451 | - if (!form.withdrawal_method) { | 395 | + if (value === undefined || value === null || value === '') { |
| 452 | - Taro.showToast({ title: '请选择提取方式', icon: 'none' }) | 396 | + Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) |
| 453 | return false | 397 | return false |
| 454 | } | 398 | } |
| 455 | - | ||
| 456 | - if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') { | ||
| 457 | - Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' }) | ||
| 458 | - return false | ||
| 459 | - } | ||
| 460 | - | ||
| 461 | - if (!form.withdrawal_period) { | ||
| 462 | - Taro.showToast({ title: '请选择提取期', icon: 'none' }) | ||
| 463 | - return false | ||
| 464 | } | 399 | } |
| 465 | 400 | ||
| 466 | - if (form.withdrawal_method === '按年岁') { | 401 | + if (field.type === 'percentage' && isFieldVisible(field)) { |
| 467 | - if (!form.annual_withdrawal_amount || form.annual_withdrawal_amount === '') { | 402 | + const percentage = parseFloat(form[field.key]) |
| 468 | - Taro.showToast({ title: '请输入每年提取金额', icon: 'none' }) | ||
| 469 | - return false | ||
| 470 | - } | ||
| 471 | - if (form.annual_increase_percentage === undefined || form.annual_increase_percentage === '') { | ||
| 472 | - Taro.showToast({ title: '请输入每年递增提取之百分比', icon: 'none' }) | ||
| 473 | - return false | ||
| 474 | - } | ||
| 475 | - | ||
| 476 | - // 验证百分比范围 | ||
| 477 | - const percentage = parseFloat(form.annual_increase_percentage) | ||
| 478 | if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { | 403 | if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { |
| 479 | Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | 404 | Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) |
| 480 | return false | 405 | return false |
| 481 | } | 406 | } |
| 482 | } | 407 | } |
| 483 | - } else if (form.withdrawal_mode === '最高固定提取金额') { | ||
| 484 | - if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') { | ||
| 485 | - Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' }) | ||
| 486 | - return false | ||
| 487 | - } | ||
| 488 | - if (!form.withdrawal_period) { | ||
| 489 | - Taro.showToast({ title: '请选择提取期', icon: 'none' }) | ||
| 490 | - return false | ||
| 491 | - } | ||
| 492 | - } | ||
| 493 | } | 408 | } |
| 494 | 409 | ||
| 495 | return true | 410 | return true | ... | ... |
| ... | @@ -19,6 +19,61 @@ | ... | @@ -19,6 +19,61 @@ |
| 19 | * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | 19 | * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key |
| 20 | * } | 20 | * } |
| 21 | */ | 21 | */ |
| 22 | +// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 23 | +const baseSubmitMapping = { | ||
| 24 | + customer_name: { api_field: 'customer_name' }, | ||
| 25 | + gender: { api_field: 'customer_gender' }, | ||
| 26 | + birthday: { api_field: 'customer_birthday' }, | ||
| 27 | + smoker: { api_field: 'smoking_status' }, | ||
| 28 | + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 29 | + payment_period: { api_field: 'payment_years' }, | ||
| 30 | + total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 34 | +const savingsSubmitMapping = { | ||
| 35 | + ...baseSubmitMapping, | ||
| 36 | + withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 37 | + withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 38 | + withdrawal_start_age: { api_field: 'withdrawal_start_age' }, | ||
| 39 | + withdrawal_period: { api_field: 'withdrawal_period' }, | ||
| 40 | + withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 41 | + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 42 | + annual_increase_percentage: { api_field: 'annual_increase_percentage' } | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 46 | +const savingsFormSchema = { | ||
| 47 | + // 基础字段:非提取计划部分 | ||
| 48 | + base_fields: [ | ||
| 49 | + { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 50 | + { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 51 | + { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 52 | + { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 53 | + { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 54 | + { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 55 | + ], | ||
| 56 | + // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 57 | + withdrawal_fields: [ | ||
| 58 | + { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 59 | + { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 60 | + { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 61 | + { 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: '按年岁' }] }, | ||
| 62 | + { id: 'withdrawal_start_age_by_year', key: 'withdrawal_start_age', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 63 | + { 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: '指定提取金额' }] }, | ||
| 64 | + { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }, { field: 'withdrawal_method', equals: '按年岁' }] }, | ||
| 65 | + { id: 'withdrawal_start_age_by_fixed', key: 'withdrawal_start_age', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 66 | + { 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: '最高固定提取金额' }] } | ||
| 67 | + ], | ||
| 68 | + // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 69 | + reset_map: { | ||
| 70 | + withdrawal_mode: { | ||
| 71 | + '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'], | ||
| 72 | + '指定提取金额': ['withdrawal_start_age', 'withdrawal_period'] | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 22 | export const PLAN_TEMPLATES = { | 77 | export const PLAN_TEMPLATES = { |
| 23 | // 人寿保险产品 - WIOP3E | 78 | // 人寿保险产品 - WIOP3E |
| 24 | 'life-insurance-wiop3e': { | 79 | 'life-insurance-wiop3e': { |
| ... | @@ -33,7 +88,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -33,7 +88,8 @@ export const PLAN_TEMPLATES = { |
| 33 | '10 年(0-70 岁)' | 88 | '10 年(0-70 岁)' |
| 34 | ], | 89 | ], |
| 35 | age_range: { min: 0, max: 75 }, // 年龄范围 | 90 | age_range: { min: 0, max: 75 }, // 年龄范围 |
| 36 | - insurance_period: '终身' // 保险期间 | 91 | + insurance_period: '终身', // 保险期间 |
| 92 | + submit_mapping: baseSubmitMapping | ||
| 37 | } | 93 | } |
| 38 | }, | 94 | }, |
| 39 | 95 | ||
| ... | @@ -49,7 +105,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -49,7 +105,8 @@ export const PLAN_TEMPLATES = { |
| 49 | '10 年(0-70 岁)' | 105 | '10 年(0-70 岁)' |
| 50 | ], | 106 | ], |
| 51 | age_range: { min: 0, max: 75 }, | 107 | age_range: { min: 0, max: 75 }, |
| 52 | - insurance_period: '终身' | 108 | + insurance_period: '终身', |
| 109 | + submit_mapping: baseSubmitMapping | ||
| 53 | } | 110 | } |
| 54 | }, | 111 | }, |
| 55 | 112 | ||
| ... | @@ -65,7 +122,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -65,7 +122,8 @@ export const PLAN_TEMPLATES = { |
| 65 | '25 年(15 日 - 60 岁)' | 122 | '25 年(15 日 - 60 岁)' |
| 66 | ], | 123 | ], |
| 67 | age_range: { min: 0, max: 65 }, | 124 | age_range: { min: 0, max: 65 }, |
| 68 | - insurance_period: '终身' | 125 | + insurance_period: '终身', |
| 126 | + submit_mapping: baseSubmitMapping | ||
| 69 | } | 127 | } |
| 70 | }, | 128 | }, |
| 71 | 129 | ||
| ... | @@ -81,7 +139,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -81,7 +139,8 @@ export const PLAN_TEMPLATES = { |
| 81 | '25 年(15 日 - 60 岁)' | 139 | '25 年(15 日 - 60 岁)' |
| 82 | ], | 140 | ], |
| 83 | age_range: { min: 0, max: 65 }, | 141 | age_range: { min: 0, max: 65 }, |
| 84 | - insurance_period: '终身' | 142 | + insurance_period: '终身', |
| 143 | + submit_mapping: baseSubmitMapping | ||
| 85 | } | 144 | } |
| 86 | }, | 145 | }, |
| 87 | 146 | ||
| ... | @@ -97,7 +156,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -97,7 +156,8 @@ export const PLAN_TEMPLATES = { |
| 97 | '25 年(15 日 - 60 岁)' | 156 | '25 年(15 日 - 60 岁)' |
| 98 | ], | 157 | ], |
| 99 | age_range: { min: 0, max: 65 }, | 158 | age_range: { min: 0, max: 65 }, |
| 100 | - insurance_period: '终身' | 159 | + insurance_period: '终身', |
| 160 | + submit_mapping: baseSubmitMapping | ||
| 101 | } | 161 | } |
| 102 | }, | 162 | }, |
| 103 | 163 | ||
| ... | @@ -124,10 +184,7 @@ export const PLAN_TEMPLATES = { | ... | @@ -124,10 +184,7 @@ export const PLAN_TEMPLATES = { |
| 124 | enabled: true, | 184 | enabled: true, |
| 125 | currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | 185 | currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 |
| 126 | default_currency: 'USD', // 统一为美元 | 186 | default_currency: 'USD', // 统一为美元 |
| 127 | - withdrawal_modes: [ | 187 | + withdrawal_modes: ['指定提取金额', '最高固定提取金额'], |
| 128 | - '年龄指定金额', // 方式1 | ||
| 129 | - '最高固定金额' // 方式2 | ||
| 130 | - ], | ||
| 131 | withdrawal_periods: [ | 188 | withdrawal_periods: [ |
| 132 | '1年', | 189 | '1年', |
| 133 | '2年', | 190 | '2年', |
| ... | @@ -138,7 +195,9 @@ export const PLAN_TEMPLATES = { | ... | @@ -138,7 +195,9 @@ export const PLAN_TEMPLATES = { |
| 138 | '20年', | 195 | '20年', |
| 139 | '终身' | 196 | '终身' |
| 140 | ] | 197 | ] |
| 141 | - } | 198 | + }, |
| 199 | + form_schema: savingsFormSchema, | ||
| 200 | + submit_mapping: savingsSubmitMapping | ||
| 142 | } | 201 | } |
| 143 | }, | 202 | }, |
| 144 | 203 | ||
| ... | @@ -160,7 +219,7 @@ export const PLAN_TEMPLATES = { | ... | @@ -160,7 +219,7 @@ export const PLAN_TEMPLATES = { |
| 160 | enabled: true, | 219 | enabled: true, |
| 161 | currencies: ['HKD', 'USD', 'CNY'], | 220 | currencies: ['HKD', 'USD', 'CNY'], |
| 162 | default_currency: 'USD', // 统一为美元 | 221 | default_currency: 'USD', // 统一为美元 |
| 163 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | 222 | + withdrawal_modes: ['指定提取金额', '最高固定提取金额'], |
| 164 | withdrawal_periods: [ | 223 | withdrawal_periods: [ |
| 165 | '1年', | 224 | '1年', |
| 166 | '2年', | 225 | '2年', |
| ... | @@ -171,7 +230,9 @@ export const PLAN_TEMPLATES = { | ... | @@ -171,7 +230,9 @@ export const PLAN_TEMPLATES = { |
| 171 | '20年', | 230 | '20年', |
| 172 | '终身' | 231 | '终身' |
| 173 | ] | 232 | ] |
| 174 | - } | 233 | + }, |
| 234 | + form_schema: savingsFormSchema, | ||
| 235 | + submit_mapping: savingsSubmitMapping | ||
| 175 | } | 236 | } |
| 176 | }, | 237 | }, |
| 177 | 238 | ||
| ... | @@ -193,7 +254,7 @@ export const PLAN_TEMPLATES = { | ... | @@ -193,7 +254,7 @@ export const PLAN_TEMPLATES = { |
| 193 | enabled: true, | 254 | enabled: true, |
| 194 | currencies: ['HKD', 'USD', 'CNY'], | 255 | currencies: ['HKD', 'USD', 'CNY'], |
| 195 | default_currency: 'USD', // 统一为美元 | 256 | default_currency: 'USD', // 统一为美元 |
| 196 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | 257 | + withdrawal_modes: ['指定提取金额', '最高固定提取金额'], |
| 197 | withdrawal_periods: [ | 258 | withdrawal_periods: [ |
| 198 | '1年', | 259 | '1年', |
| 199 | '2年', | 260 | '2年', |
| ... | @@ -204,7 +265,9 @@ export const PLAN_TEMPLATES = { | ... | @@ -204,7 +265,9 @@ export const PLAN_TEMPLATES = { |
| 204 | '20年', | 265 | '20年', |
| 205 | '终身' | 266 | '终身' |
| 206 | ] | 267 | ] |
| 207 | - } | 268 | + }, |
| 269 | + form_schema: savingsFormSchema, | ||
| 270 | + submit_mapping: savingsSubmitMapping | ||
| 208 | } | 271 | } |
| 209 | }, | 272 | }, |
| 210 | 273 | ||
| ... | @@ -227,7 +290,7 @@ export const PLAN_TEMPLATES = { | ... | @@ -227,7 +290,7 @@ export const PLAN_TEMPLATES = { |
| 227 | enabled: true, | 290 | enabled: true, |
| 228 | currencies: ['HKD', 'USD', 'CNY'], | 291 | currencies: ['HKD', 'USD', 'CNY'], |
| 229 | default_currency: 'USD', // 统一为美元 | 292 | default_currency: 'USD', // 统一为美元 |
| 230 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | 293 | + withdrawal_modes: ['指定提取金额', '最高固定提取金额'], |
| 231 | withdrawal_periods: [ | 294 | withdrawal_periods: [ |
| 232 | '1年', | 295 | '1年', |
| 233 | '2年', | 296 | '2年', |
| ... | @@ -238,7 +301,9 @@ export const PLAN_TEMPLATES = { | ... | @@ -238,7 +301,9 @@ export const PLAN_TEMPLATES = { |
| 238 | '20年', | 301 | '20年', |
| 239 | '终身' | 302 | '终身' |
| 240 | ] | 303 | ] |
| 241 | - } | 304 | + }, |
| 305 | + form_schema: savingsFormSchema, | ||
| 306 | + submit_mapping: savingsSubmitMapping | ||
| 242 | } | 307 | } |
| 243 | } | 308 | } |
| 244 | } | 309 | } | ... | ... |
-
Please register or login to post a comment