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,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_enabledwithdrawal_modewithdrawal_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,
......