refactor(plan): 重构多阶段提取方案 UI 渲染逻辑
- 统一单阶段和多阶段的 UI 渲染流程 - 新增 shouldRenderField() 方法智能控制字段显示 - 多阶段模式现在支持"指定提取金额"和"最高固定提取金额"两种方式 - 优化校验逻辑:基础字段 → withdrawal_fields → 多阶段卡片 - 关闭 Mock 数据(准备联调测试) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
4 changed files
with
142 additions
and
127 deletions
| ... | @@ -83,3 +83,20 @@ | ... | @@ -83,3 +83,20 @@ |
| 83 | 83 | ||
| 84 | **变更摘要**: | 84 | **变更摘要**: |
| 85 | - 无详细描述 | 85 | - 无详细描述 |
| 86 | +## 2026-02-27 | ||
| 87 | + | ||
| 88 | +### 09:56:15 - 完成任务 | ||
| 89 | + | ||
| 90 | +**影响文件**: | ||
| 91 | +- `src/utils/README.md` | ||
| 92 | + | ||
| 93 | +**变更摘要**: | ||
| 94 | +- 无详细描述 | ||
| 95 | + | ||
| 96 | +### 10:00:56 - 完成任务 | ||
| 97 | + | ||
| 98 | +**影响文件**: | ||
| 99 | +- `src/utils/README.md` | ||
| 100 | + | ||
| 101 | +**变更摘要**: | ||
| 102 | +- 无详细描述 | ... | ... |
| ... | @@ -25,104 +25,22 @@ | ... | @@ -25,104 +25,22 @@ |
| 25 | 25 | ||
| 26 | <div class="border-t border-gray-200 my-6"></div> | 26 | <div class="border-t border-gray-200 my-6"></div> |
| 27 | 27 | ||
| 28 | - <!-- 多阶段提取计划(优先显示) --> | 28 | + <!-- 提取计划(单阶段和多阶段通用逻辑) --> |
| 29 | - <div v-if="isMultiStageMode" class="multi-stage-withdrawal-section"> | 29 | + <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> |
| 30 | - <h3 class="text-base font-semibold text-gray-900 mb-4"> | 30 | + <!-- 1. 渲染 withdrawal_fields(包含 withdrawal_enabled、withdrawal_mode、withdrawal_method 等) --> |
| 31 | - 款项提取(允许减少名义金额) | 31 | + <!-- 注意:多阶段模式 + 选择"指定提取金额"时,跳过单组字段渲染 --> |
| 32 | - </h3> | ||
| 33 | - | ||
| 34 | - <!-- 阶段卡片列表 --> | ||
| 35 | - <div | ||
| 36 | - v-for="(stage, index) in stages" | ||
| 37 | - :key="index" | ||
| 38 | - class="stage-card bg-white border border-gray-200 rounded-lg p-4 mb-4" | ||
| 39 | - > | ||
| 40 | - <!-- 阶段标题 --> | ||
| 41 | - <div class="flex items-center justify-between mb-3"> | ||
| 42 | - <h4 class="text-sm font-medium text-gray-900">阶段{{ index + 1 }}</h4> | ||
| 43 | - <!-- 删除按钮(≥12岁且至少有2个阶段时显示) --> | ||
| 44 | - <nut-button | ||
| 45 | - v-if="canRemoveStage && index > 0" | ||
| 46 | - size="small" | ||
| 47 | - type="danger" | ||
| 48 | - @click="removeStage(index)" | ||
| 49 | - > | ||
| 50 | - 删除 | ||
| 51 | - </nut-button> | ||
| 52 | - </div> | ||
| 53 | - | ||
| 54 | - <!-- 每年提取金额 --> | ||
| 55 | - <PlanFieldAmount | ||
| 56 | - v-model="stage.annual_withdrawal_amount" | ||
| 57 | - label="每年提取金额" | ||
| 58 | - placeholder="请输入每年提取金额" | ||
| 59 | - inputLabel="请输入每年提取金额" | ||
| 60 | - :required="true" | ||
| 61 | - :currency="config.withdrawal_plan?.default_currency || config.currency" | ||
| 62 | - class="mb-3" | ||
| 63 | - /> | ||
| 64 | - | ||
| 65 | - <!-- 由几岁开始 --> | ||
| 66 | - <PlanFieldAgePicker | ||
| 67 | - v-model="stage.withdrawal_start_age" | ||
| 68 | - label="由几岁开始" | ||
| 69 | - placeholder="请输入开始提取年龄" | ||
| 70 | - :required="true" | ||
| 71 | - class="mb-3" | ||
| 72 | - /> | ||
| 73 | - | ||
| 74 | - <!-- 提取期 --> | ||
| 75 | - <PlanFieldSelect | ||
| 76 | - v-model="stage.withdrawal_period" | ||
| 77 | - label="提取期" | ||
| 78 | - placeholder="请选择提取期" | ||
| 79 | - :required="true" | ||
| 80 | - :options="multiStagePeriodOptions" | ||
| 81 | - class="mb-3" | ||
| 82 | - /> | ||
| 83 | - | ||
| 84 | - <!-- 每年递增提取之百分比(可选) --> | ||
| 85 | - <div class="percentage-field"> | ||
| 86 | - <div class="text-sm text-gray-700 mb-2 flex items-center"> | ||
| 87 | - <span>每年递增提取之百分比(%)</span> | ||
| 88 | - <span class="text-gray-400 text-xs ml-2">(可选)</span> | ||
| 89 | - </div> | ||
| 90 | - <nut-input | ||
| 91 | - v-model="stage.annual_increase_percentage" | ||
| 92 | - type="digit" | ||
| 93 | - placeholder="请输入递增百分比" | ||
| 94 | - @input="(value) => onPercentageInput(value, `stages.${index}.annual_increase_percentage`)" | ||
| 95 | - class="w-full" | ||
| 96 | - /> | ||
| 97 | - </div> | ||
| 98 | - </div> | ||
| 99 | - | ||
| 100 | - <!-- 添加阶段按钮(≥12岁且未达上限时显示) --> | ||
| 101 | - <nut-button | ||
| 102 | - v-if="canAddStage" | ||
| 103 | - type="primary" | ||
| 104 | - block | ||
| 105 | - @click="addStage" | ||
| 106 | - class="add-stage-btn" | ||
| 107 | - > | ||
| 108 | - + 添加阶段 | ||
| 109 | - </nut-button> | ||
| 110 | - </div> | ||
| 111 | - | ||
| 112 | - <!-- 单阶段提取计划(原有逻辑) --> | ||
| 113 | - <div v-else-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> | ||
| 114 | <template v-for="field in withdrawalFields" :key="field.id || field.key"> | 32 | <template v-for="field in withdrawalFields" :key="field.id || field.key"> |
| 115 | <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4"> | 33 | <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4"> |
| 116 | {{ field.section_title }} | 34 | {{ field.section_title }} |
| 117 | </h3> | 35 | </h3> |
| 118 | <component | 36 | <component |
| 119 | - v-if="isFieldVisible(field.key) && field.type !== 'percentage'" | 37 | + v-if="shouldRenderField(field) && field.type !== 'percentage'" |
| 120 | :is="getFieldComponent(field)" | 38 | :is="getFieldComponent(field)" |
| 121 | v-model="form[field.key]" | 39 | v-model="form[field.key]" |
| 122 | v-bind="getFieldProps(field)" | 40 | v-bind="getFieldProps(field)" |
| 123 | class="mb-5" | 41 | class="mb-5" |
| 124 | /> | 42 | /> |
| 125 | - <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5"> | 43 | + <div v-else-if="shouldRenderField(field) && field.type === 'percentage'" class="mb-5"> |
| 126 | <div class="text-sm text-gray-700 mb-2 flex items-center"> | 44 | <div class="text-sm text-gray-700 mb-2 flex items-center"> |
| 127 | <span v-if="field.required" class="text-red-500 mr-1">*</span> | 45 | <span v-if="field.required" class="text-red-500 mr-1">*</span> |
| 128 | <span>{{ field.label }}</span> | 46 | <span>{{ field.label }}</span> |
| ... | @@ -136,6 +54,86 @@ | ... | @@ -136,6 +54,86 @@ |
| 136 | /> | 54 | /> |
| 137 | </div> | 55 | </div> |
| 138 | </template> | 56 | </template> |
| 57 | + | ||
| 58 | + <!-- 2. 多阶段模式 + 选择"指定提取金额":显示多组阶段卡片 --> | ||
| 59 | + <div v-if="isMultiStageMode && form.withdrawal_mode === '指定提取金额'" class="multi-stage-withdrawal-section"> | ||
| 60 | + <!-- 阶段卡片列表 --> | ||
| 61 | + <div | ||
| 62 | + v-for="(stage, index) in stages" | ||
| 63 | + :key="index" | ||
| 64 | + class="stage-card bg-white border border-gray-200 rounded-lg p-4 mb-4" | ||
| 65 | + > | ||
| 66 | + <!-- 阶段标题 --> | ||
| 67 | + <div class="flex items-center justify-between mb-3"> | ||
| 68 | + <h4 class="text-sm font-medium text-gray-900">阶段{{ index + 1 }}</h4> | ||
| 69 | + <!-- 删除按钮(≥12岁且至少有2个阶段时显示) --> | ||
| 70 | + <nut-button | ||
| 71 | + v-if="canRemoveStage && index > 0" | ||
| 72 | + size="small" | ||
| 73 | + type="danger" | ||
| 74 | + @click="removeStage(index)" | ||
| 75 | + > | ||
| 76 | + 删除 | ||
| 77 | + </nut-button> | ||
| 78 | + </div> | ||
| 79 | + | ||
| 80 | + <!-- 每年提取金额 --> | ||
| 81 | + <PlanFieldAmount | ||
| 82 | + v-model="stage.annual_withdrawal_amount" | ||
| 83 | + label="每年提取金额" | ||
| 84 | + placeholder="请输入每年提取金额" | ||
| 85 | + inputLabel="请输入每年提取金额" | ||
| 86 | + :required="true" | ||
| 87 | + :currency="config.withdrawal_plan?.default_currency || config.currency" | ||
| 88 | + class="mb-3" | ||
| 89 | + /> | ||
| 90 | + | ||
| 91 | + <!-- 由几岁开始 --> | ||
| 92 | + <PlanFieldAgePicker | ||
| 93 | + v-model="stage.withdrawal_start_age" | ||
| 94 | + label="由几岁开始" | ||
| 95 | + placeholder="请输入开始提取年龄" | ||
| 96 | + :required="true" | ||
| 97 | + class="mb-3" | ||
| 98 | + /> | ||
| 99 | + | ||
| 100 | + <!-- 提取期 --> | ||
| 101 | + <PlanFieldSelect | ||
| 102 | + v-model="stage.withdrawal_period" | ||
| 103 | + label="提取期" | ||
| 104 | + placeholder="请选择提取期" | ||
| 105 | + :required="true" | ||
| 106 | + :options="multiStagePeriodOptions" | ||
| 107 | + class="mb-3" | ||
| 108 | + /> | ||
| 109 | + | ||
| 110 | + <!-- 每年递增提取之百分比(可选) --> | ||
| 111 | + <div class="percentage-field"> | ||
| 112 | + <div class="text-sm text-gray-700 mb-2 flex items-center"> | ||
| 113 | + <span>每年递增提取之百分比(%)</span> | ||
| 114 | + <span class="text-gray-400 text-xs ml-2">(可选)</span> | ||
| 115 | + </div> | ||
| 116 | + <nut-input | ||
| 117 | + v-model="stage.annual_increase_percentage" | ||
| 118 | + type="digit" | ||
| 119 | + placeholder="请输入递增百分比" | ||
| 120 | + @input="(value) => onPercentageInput(value, `stages.${index}.annual_increase_percentage`)" | ||
| 121 | + class="w-full" | ||
| 122 | + /> | ||
| 123 | + </div> | ||
| 124 | + </div> | ||
| 125 | + | ||
| 126 | + <!-- 添加阶段按钮(≥12岁且未达上限时显示) --> | ||
| 127 | + <nut-button | ||
| 128 | + v-if="canAddStage" | ||
| 129 | + type="primary" | ||
| 130 | + block | ||
| 131 | + @click="addStage" | ||
| 132 | + class="add-stage-btn" | ||
| 133 | + > | ||
| 134 | + + 添加阶段 | ||
| 135 | + </nut-button> | ||
| 136 | + </div> | ||
| 139 | </div> | 137 | </div> |
| 140 | </div> | 138 | </div> |
| 141 | 139 | ||
| ... | @@ -309,6 +307,32 @@ const getFieldProps = (field) => { | ... | @@ -309,6 +307,32 @@ const getFieldProps = (field) => { |
| 309 | 307 | ||
| 310 | const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions) | 308 | const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions) |
| 311 | 309 | ||
| 310 | +/** | ||
| 311 | + * 判断字段是否应该渲染 | ||
| 312 | + * @description 多阶段模式 + 选择"指定提取金额"时,跳过单组字段渲染 | ||
| 313 | + * @param {Object} field - 字段配置 | ||
| 314 | + * @returns {boolean} 是否应该渲染 | ||
| 315 | + */ | ||
| 316 | +const shouldRenderField = (field) => { | ||
| 317 | + // 基础可见性检查 | ||
| 318 | + if (!isFieldVisible(field.key)) return false | ||
| 319 | + | ||
| 320 | + // 多阶段模式 + 选择"指定提取金额"时,跳过部分单组字段 | ||
| 321 | + // 注意:withdrawal_method(提取方式)需要保留 | ||
| 322 | + if (isMultiStageMode.value && form.withdrawal_mode === '指定提取金额') { | ||
| 323 | + // 需要跳过的字段:单阶段"指定提取金额"的金额和期数相关字段 | ||
| 324 | + const skipFields = [ | ||
| 325 | + 'annual_withdrawal_amount', | ||
| 326 | + 'withdrawal_start_age_specified', | ||
| 327 | + 'withdrawal_period_specified', | ||
| 328 | + 'annual_increase_percentage' | ||
| 329 | + ] | ||
| 330 | + if (skipFields.includes(field.key)) return false | ||
| 331 | + } | ||
| 332 | + | ||
| 333 | + return true | ||
| 334 | +} | ||
| 335 | + | ||
| 312 | // ====== 多阶段提取计划逻辑 ====== | 336 | // ====== 多阶段提取计划逻辑 ====== |
| 313 | 337 | ||
| 314 | /** | 338 | /** |
| ... | @@ -681,12 +705,7 @@ const isFieldRequired = (field) => { | ... | @@ -681,12 +705,7 @@ const isFieldRequired = (field) => { |
| 681 | * @returns {boolean} 校验是否通过 | 705 | * @returns {boolean} 校验是否通过 |
| 682 | */ | 706 | */ |
| 683 | const validate = () => { | 707 | const validate = () => { |
| 684 | - // 多阶段模式校验 | 708 | + // 1. 基础字段校验(单阶段和多阶段通用) |
| 685 | - if (isMultiStageMode.value) { | ||
| 686 | - return validateMultiStage() | ||
| 687 | - } | ||
| 688 | - | ||
| 689 | - // 单阶段模式校验(原有逻辑) | ||
| 690 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] | 709 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] |
| 691 | 710 | ||
| 692 | // 年龄与出生年月日二选一校验 | 711 | // 年龄与出生年月日二选一校验 |
| ... | @@ -705,6 +724,7 @@ const validate = () => { | ... | @@ -705,6 +724,7 @@ const validate = () => { |
| 705 | form.age = currentYear - birthYear | 724 | form.age = currentYear - birthYear |
| 706 | } | 725 | } |
| 707 | 726 | ||
| 727 | + // 2. 校验基础字段和 withdrawal_fields 中可见的必填字段 | ||
| 708 | for (const field of fields) { | 728 | for (const field of fields) { |
| 709 | if (!isFieldVisible(field.key)) { | 729 | if (!isFieldVisible(field.key)) { |
| 710 | continue | 730 | continue |
| ... | @@ -733,44 +753,21 @@ const validate = () => { | ... | @@ -733,44 +753,21 @@ const validate = () => { |
| 733 | } | 753 | } |
| 734 | } | 754 | } |
| 735 | 755 | ||
| 756 | + // 3. 多阶段模式 + 选择"指定提取金额":额外校验多阶段卡片 | ||
| 757 | + if (isMultiStageMode.value && form.withdrawal_mode === '指定提取金额') { | ||
| 758 | + return validateMultiStage() | ||
| 759 | + } | ||
| 760 | + | ||
| 736 | return true | 761 | return true |
| 737 | } | 762 | } |
| 738 | 763 | ||
| 739 | /** | 764 | /** |
| 740 | * 多阶段表单校验 | 765 | * 多阶段表单校验 |
| 741 | - * @description 校验每个阶段的必填字段 | 766 | + * @description 校验多阶段卡片的必填字段 |
| 767 | + * @note 基础字段和 withdrawal_fields 已在 validate() 中校验 | ||
| 742 | * @returns {boolean} 校验是否通过 | 768 | * @returns {boolean} 校验是否通过 |
| 743 | */ | 769 | */ |
| 744 | const validateMultiStage = () => { | 770 | const validateMultiStage = () => { |
| 745 | - // 基础字段校验(年龄与出生年月日) | ||
| 746 | - const hasAge = !isEmptyValue(form.age) | ||
| 747 | - const hasBirthday = !isEmptyValue(form.birthday) | ||
| 748 | - | ||
| 749 | - if (!hasAge && !hasBirthday) { | ||
| 750 | - Taro.showToast({ title: '年龄与出生年月日至少填写一项', icon: 'none' }) | ||
| 751 | - return false | ||
| 752 | - } | ||
| 753 | - | ||
| 754 | - // 如果都填写了,以生日为准,重新计算年龄 | ||
| 755 | - if (hasAge && hasBirthday) { | ||
| 756 | - const birthYear = new Date(form.birthday).getFullYear() | ||
| 757 | - const currentYear = new Date().getFullYear() | ||
| 758 | - form.age = currentYear - birthYear | ||
| 759 | - } | ||
| 760 | - | ||
| 761 | - // 基础字段校验(其他字段) | ||
| 762 | - for (const field of baseFields.value) { | ||
| 763 | - if (field.key === 'age') continue // 已处理 | ||
| 764 | - | ||
| 765 | - if (isFieldRequired(field)) { | ||
| 766 | - const value = form[field.key] | ||
| 767 | - if (isEmptyValue(value)) { | ||
| 768 | - Taro.showToast({ title: getRequiredMessage(field), icon: 'none' }) | ||
| 769 | - return false | ||
| 770 | - } | ||
| 771 | - } | ||
| 772 | - } | ||
| 773 | - | ||
| 774 | // 多阶段字段校验 | 771 | // 多阶段字段校验 |
| 775 | for (let i = 0; i < stages.value.length; i++) { | 772 | for (let i = 0; i < stages.value.length; i++) { |
| 776 | const stage = stages.value[i] | 773 | const stage = stages.value[i] | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2026-02-13 01:05:52 | 2 | * @Date: 2026-02-13 01:05:52 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-02-25 17:18:05 | 4 | + * @LastEditTime: 2026-02-27 10:22:51 |
| 5 | * @FilePath: /manulife-weapp/src/config/app.js | 5 | * @FilePath: /manulife-weapp/src/config/app.js |
| 6 | * @Description: 应用配置 | 6 | * @Description: 应用配置 |
| 7 | */ | 7 | */ |
| ... | @@ -29,7 +29,7 @@ | ... | @@ -29,7 +29,7 @@ |
| 29 | * // 关闭 Mock 数据(生产环境) | 29 | * // 关闭 Mock 数据(生产环境) |
| 30 | * USE_MOCK_DATA = false | 30 | * USE_MOCK_DATA = false |
| 31 | */ | 31 | */ |
| 32 | -export const USE_MOCK_DATA = true | 32 | +export const USE_MOCK_DATA = false |
| 33 | 33 | ||
| 34 | /** | 34 | /** |
| 35 | * 根据 NODE_ENV 自动判断是否使用 Mock | 35 | * 根据 NODE_ENV 自动判断是否使用 Mock | ... | ... |
| ... | @@ -393,7 +393,8 @@ export const PLAN_TEMPLATES = { | ... | @@ -393,7 +393,8 @@ export const PLAN_TEMPLATES = { |
| 393 | enabled: true, | 393 | enabled: true, |
| 394 | currencies: ['HKD', 'USD', 'CNY'], | 394 | currencies: ['HKD', 'USD', 'CNY'], |
| 395 | default_currency: 'USD', | 395 | default_currency: 'USD', |
| 396 | - withdrawal_modes: ['指定提取金额'], // 多阶段模式只支持指定提取金额 | 396 | + // 多阶段模式:支持指定提取金额(多组)和最高固定提取金额(单组) |
| 397 | + withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 397 | withdrawal_periods: multiStageWithdrawalConfig.withdrawal_periods | 398 | withdrawal_periods: multiStageWithdrawalConfig.withdrawal_periods |
| 398 | }, | 399 | }, |
| 399 | form_schema: savingsFormSchema, | 400 | form_schema: savingsFormSchema, | ... | ... |
-
Please register or login to post a comment