hookehuyr

refactor(plan): 重构多阶段提取方案 UI 渲染逻辑

- 统一单阶段和多阶段的 UI 渲染流程
- 新增 shouldRenderField() 方法智能控制字段显示
- 多阶段模式现在支持"指定提取金额"和"最高固定提取金额"两种方式
- 优化校验逻辑:基础字段 → withdrawal_fields → 多阶段卡片
- 关闭 Mock 数据(准备联调测试)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -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,12 +25,38 @@ ...@@ -25,12 +25,38 @@
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_enabledwithdrawal_modewithdrawal_method 等) -->
31 - 款项提取(允许减少名义金额) 31 + <!-- 注意:多阶段模式 + 选择"指定提取金额"时,跳过单组字段渲染 -->
32 + <template v-for="field in withdrawalFields" :key="field.id || field.key">
33 + <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4">
34 + {{ field.section_title }}
32 </h3> 35 </h3>
36 + <component
37 + v-if="shouldRenderField(field) && field.type !== 'percentage'"
38 + :is="getFieldComponent(field)"
39 + v-model="form[field.key]"
40 + v-bind="getFieldProps(field)"
41 + class="mb-5"
42 + />
43 + <div v-else-if="shouldRenderField(field) && field.type === 'percentage'" class="mb-5">
44 + <div class="text-sm text-gray-700 mb-2 flex items-center">
45 + <span v-if="field.required" class="text-red-500 mr-1">*</span>
46 + <span>{{ field.label }}</span>
47 + </div>
48 + <nut-input
49 + v-model="form[field.key]"
50 + type="digit"
51 + :placeholder="field.placeholder"
52 + @input="(value) => onPercentageInput(value, field.key)"
53 + class="w-full"
54 + />
55 + </div>
56 + </template>
33 57
58 + <!-- 2. 多阶段模式 + 选择"指定提取金额":显示多组阶段卡片 -->
59 + <div v-if="isMultiStageMode && form.withdrawal_mode === '指定提取金额'" class="multi-stage-withdrawal-section">
34 <!-- 阶段卡片列表 --> 60 <!-- 阶段卡片列表 -->
35 <div 61 <div
36 v-for="(stage, index) in stages" 62 v-for="(stage, index) in stages"
...@@ -108,34 +134,6 @@ ...@@ -108,34 +134,6 @@
108 + 添加阶段 134 + 添加阶段
109 </nut-button> 135 </nut-button>
110 </div> 136 </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">
115 - <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4">
116 - {{ field.section_title }}
117 - </h3>
118 - <component
119 - v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
120 - :is="getFieldComponent(field)"
121 - v-model="form[field.key]"
122 - v-bind="getFieldProps(field)"
123 - class="mb-5"
124 - />
125 - <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
126 - <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>
128 - <span>{{ field.label }}</span>
129 - </div>
130 - <nut-input
131 - v-model="form[field.key]"
132 - type="digit"
133 - :placeholder="field.placeholder"
134 - @input="(value) => onPercentageInput(value, field.key)"
135 - class="w-full"
136 - />
137 - </div>
138 - </template>
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,
......