hookehuyr

feat(plan): 新增多阶段提取方案功能

- 新增产品:宏挚传承保障计划(多阶段) (savings-gs-multistage)
- 年龄 < 12岁:固定显示 3 个阶段
- 年龄 ≥ 12岁:初始 1 个阶段,可添加至 4 个
- 提取期新增"一笔过"选项
- 递增百分比改为可选字段(不填传 null)
- 提交数据格式:withdrawal_stages 数组
- 删除冗余的 mock 文件 (src/api/mock/hotProducts.js)
- 文档归档到 docs/tasks/2026-02-25/archive/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -2,6 +2,29 @@ ...@@ -2,6 +2,29 @@
2 2
3 记录项目开发过程中的重要变更和完成任务。 3 记录项目开发过程中的重要变更和完成任务。
4 4
5 +## 2026-02-25
6 +
7 +### 21:00:00 - feat(plan): 新增多阶段提取方案功能
8 +
9 +**影响文件**:
10 +- `src/config/plan-templates.js` - 新增多阶段产品配置和 `savings-gs-multistage` 产品
11 +- `src/components/plan/PlanTemplates/SavingsTemplate.vue` - 实现多阶段提取渲染和交互逻辑
12 +- `src/api/mock/mock/hotProducts.js` - 添加多阶段产品 mock 数据
13 +
14 +**变更摘要**:
15 +- 新增产品:宏挚传承保障计划(多阶段) (`savings-gs-multistage`)
16 +- 多阶段功能:
17 + - 年龄 < 12岁:固定显示 3 个阶段
18 + - 年龄 ≥ 12岁:初始 1 个阶段,可添加至 4 个
19 + - 提取期新增"一笔过"选项
20 + - 递增百分比改为可选字段(不填传 null)
21 +- 提交数据格式:`withdrawal_stages` 数组
22 +- 样式优化:阶段卡片白色背景
23 +
24 +**相关文档**:
25 +- `docs/tasks/2026-02-25/archive/多阶段提取方案设计.md`
26 +- `docs/tasks/2026-02-25/archive/客户新需求-2026-02-25.md`
27 +
5 ## 2026-02-24 28 ## 2026-02-24
6 29
7 ### 23:59:18 - 完成任务 30 ### 23:59:18 - 完成任务
...@@ -15,3 +38,48 @@ ...@@ -15,3 +38,48 @@
15 38
16 **变更摘要**: 39 **变更摘要**:
17 - 无详细描述 40 - 无详细描述
41 +## 2026-02-25
42 +
43 +### 20:09:02 - 完成任务
44 +
45 +**影响文件**:
46 +- `src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue`
47 +- `src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue`
48 +- `src/components/plan/PlanTemplates/SavingsTemplate.vue`
49 +- `src/config/plan-templates.js`
50 +
51 +**变更摘要**:
52 +- 无详细描述
53 +
54 +### 20:11:21 - 完成任务
55 +
56 +**影响文件**:
57 +- `src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue`
58 +- `src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue`
59 +- `src/components/plan/PlanTemplates/SavingsTemplate.vue`
60 +- `src/config/plan-templates.js`
61 +
62 +**变更摘要**:
63 +- 无详细描述
64 +
65 +### 20:12:04 - 完成任务
66 +
67 +**影响文件**:
68 +- `src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue`
69 +- `src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue`
70 +- `src/components/plan/PlanTemplates/SavingsTemplate.vue`
71 +- `src/config/plan-templates.js`
72 +
73 +**变更摘要**:
74 +- 无详细描述
75 +
76 +### 20:14:14 - 完成任务
77 +
78 +**影响文件**:
79 +- `src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue`
80 +- `src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue`
81 +- `src/components/plan/PlanTemplates/SavingsTemplate.vue`
82 +- `src/config/plan-templates.js`
83 +
84 +**变更摘要**:
85 +- 无详细描述
......
1 # 多阶段提取方案设计文档 1 # 多阶段提取方案设计文档
2 2
3 > **创建时间**: 2026-02-25 3 > **创建时间**: 2026-02-25
4 -> **状态**: ⏳ 待客户确认需求 4 +> **状态**: ✅ 需求已确认
5 > **设计师**: Claude Code 5 > **设计师**: Claude Code
6 -> **版本**: 1.1.0 6 +> **版本**: 2.0.0
7 -> **更新内容**: 新增需求说明章节,辨析多阶段与多方案概念 7 +> **更新内容**: 客户确认只需多阶段提取,不需要多方案对比功能
8 8
9 --- 9 ---
10 10
11 ## 📋 目录 11 ## 📋 目录
12 12
13 -1. [需求说明(待客户确认)](#需求说明待客户确认) 13 +1. [需求说明(已确认)](#需求说明已确认)
14 2. [需求背景](#需求背景) 14 2. [需求背景](#需求背景)
15 3. [现状分析](#现状分析) 15 3. [现状分析](#现状分析)
16 4. [方案设计](#方案设计) 16 4. [方案设计](#方案设计)
...@@ -18,25 +18,20 @@ ...@@ -18,25 +18,20 @@
18 6. [组件设计](#组件设计) 18 6. [组件设计](#组件设计)
19 7. [Schema 配置](#schema-配置) 19 7. [Schema 配置](#schema-配置)
20 8. [字段映射](#字段映射) 20 8. [字段映射](#字段映射)
21 -9. [多方案需求分析](#多方案需求分析) 21 +9. [实施清单](#实施清单)
22 -10. [实施清单](#实施清单) 22 +10. [待确认事项](#待确认事项)
23 -11. [待确认事项](#待确认事项)
24 23
25 --- 24 ---
26 25
27 -## 📖 需求说明(待客户确认) 26 +## 📖 需求说明(确认)
28 27
29 -> **重要说明**:以下是我们对客户资料的理解分析,**请客户确认实际需求**。 28 +> **✅ 客户已确认**:2026-02-25
30 29
31 -### 概念辨析:多阶段 vs 多方案 30 +### 需求概述
32 31
33 -客户提供的资料中出现了两个容易混淆的概念,需要明确区分: 32 +**多阶段提取(Multi-Stage Withdrawal)****一个提取方案**中,包含**多个不同阶段**的提取计划。
34 33
35 -#### 1️⃣ 多阶段提取(Multi-Stage Withdrawal) 34 +**示例**(教育+创业+退休组合):
36 -
37 -**定义****一个提取方案**中,包含**多个不同阶段**的提取计划。
38 -
39 -**示例**(3岁小公主 - 方案一):
40 ``` 35 ```
41 同一个方案内的三个阶段: 36 同一个方案内的三个阶段:
42 ├─ 第一阶段:18-21岁,每年5万(教育基金) 37 ├─ 第一阶段:18-21岁,每年5万(教育基金)
...@@ -44,72 +39,11 @@ ...@@ -44,72 +39,11 @@
44 └─ 第三阶段:50-100岁,每年7万(退休养老年金) 39 └─ 第三阶段:50-100岁,每年7万(退休养老年金)
45 ``` 40 ```
46 41
47 -**特点** 42 +### 客户确认要点
48 -- ✅ 这是一个方案
49 -- ✅ 包含多个提取阶段
50 -- ✅ 每个阶段针对不同人生阶段的需求
51 -
52 -#### 2️⃣ 多方案(Multiple Scenarios)
53 -
54 -**定义****同一份计划书**中,展示**多个备选提取方案**供客户选择。
55 -
56 -**示例**(3岁小公主 - 3个方案):
57 -```
58 -方案一:多阶段分时提取
59 - └─ 教育(18-21岁) + 创业(30岁) + 退休(50-100岁)
60 -
61 -方案二:长期定期提取
62 - └─ 9-100岁,每年1.75万
63 -
64 -方案三:财富传承
65 - └─ 不提取,展示保单价值增长表
66 -```
67 -
68 -**特点**
69 -- ✅ 这是三个独立的方案
70 -- ✅ 每个方案代表不同的提取策略
71 -- ✅ 客户可以在方案之间对比选择
72 -
73 -### 对比总结
74 -
75 -| 维度 | 多阶段 | 多方案 |
76 -|------|--------|--------|
77 -| **是什么** | 一个方案内的多个提取阶段 | 多个独立的备选方案 |
78 -| **为什么** | 满足不同人生阶段的资金需求 | 提供不同风险/收益的提取策略 |
79 -| **数据量** | 1个方案,3-5个阶段 | 3个方案,每个方案可能有多个阶段 |
80 -| **客户选择** | 无需选择,按阶段执行 | 客户需要选择采用哪个方案 |
81 -
82 -### 需要客户确认的问题
83 -
84 -#### 问题 1:是否需要多阶段提取?
85 -
86 -**说明**:是否需要在一个方案中设置多个提取阶段(如:教育→创业→退休)?
87 -
88 -- [ ] 是,需要多阶段提取
89 -- [ ] 否,每个方案只需要一种提取方式
90 -
91 -#### 问题 2:是否需要多方案?
92 -
93 -**说明**:是否需要在同一份计划书中展示多个备选方案供客户选择?
94 -
95 -- [ ] 是,需要多方案对比(如:保守方案 vs 积极方案 vs 传承方案)
96 -- [ ] 否,每个客户只需要一个方案
97 -
98 -#### 问题 3:实施方案选择
99 43
100 -如果需要多方案,请选择实施方案: 44 +- ✅ 一份计划书里只有一个提取方案,不会有多个
101 - 45 +- ✅ 需要支持在一个方案中设置多个不同阶段的提取计划
102 -**选项 A:单次提交,多方案对比** 46 +- ✅ 阶段数量建议 1-5 个
103 -- 优点:一次性输入多个方案,计划书可直接对比展示
104 -- 缺点:需要后端改动支持,开发周期较长
105 -
106 -**选项 B:多次提交,分别生成**(推荐)
107 -- 优点:复用现有逻辑,无需后端改动,快速上线
108 -- 缺点:每个方案单独生成,需要人工对比
109 -
110 -**选项 C:混合方案**
111 -- 第一阶段:先实现选项B(快速上线)
112 -- 第二阶段:再升级为选项A(完整体验)
113 47
114 --- 48 ---
115 49
...@@ -596,189 +530,43 @@ const savingsSubmitMapping = { ...@@ -596,189 +530,43 @@ const savingsSubmitMapping = {
596 530
597 --- 531 ---
598 532
599 -## 多方案需求分析
600 -
601 -> **说明**:如果客户确认需要多方案功能,以下是技术实施建议。
602 -
603 -### 现有系统能力评估
604 -
605 -| 功能 | 现有支持 | 客户需求 | 差距 |
606 -|------|---------|---------|------|
607 -| 单个提取方案 | ✅ 完全支持 | ✅ 需要 | 无 |
608 -| 多阶段分时提取 | ❌ 不支持 | ✅ 需要 | 本文档已设计方案 |
609 -| **多个提取方案** | ❌ 不支持 | ⏳ 待确认 | **需确认后设计** |
610 -
611 -### 多方案架构设计建议
612 -```javascript
613 -formData = {
614 - // ... 基础字段
615 -
616 - withdrawal_enabled: '是',
617 -
618 - // 多方案模式
619 - withdrawal_scenarios: [
620 - {
621 - scenario_id: 1,
622 - scenario_name: '方案一:教育+创业+退休',
623 - withdrawal_mode: '多阶段分时提取',
624 - withdrawal_stages: [
625 - { stage_name: '教育基金', start_age: 18, end_age: 21, withdrawal_type: '每年提取', annual_amount: 50000 },
626 - { stage_name: '创业金', start_age: 30, end_age: 30, withdrawal_type: '一笔过', lump_sum_amount: 400000 },
627 - { stage_name: '退休金', start_age: 50, end_age: 100, withdrawal_type: '每年提取', annual_amount: 70000 }
628 - ]
629 - },
630 - {
631 - scenario_id: 2,
632 - scenario_name: '方案二:长期年金',
633 - withdrawal_mode: '单阶段定期提取',
634 - withdrawal_start_age: 9,
635 - withdrawal_period: 92, // 9-100岁
636 - annual_withdrawal_amount: 17500
637 - },
638 - {
639 - scenario_id: 3,
640 - scenario_name: '方案三:财富传承',
641 - withdrawal_mode: '不提取',
642 - withdrawal_stages: []
643 - }
644 - ]
645 -}
646 -```
647 -
648 -#### 方案 B:多次提交,分别生成方案(推荐)
649 -
650 -**优点**
651 -- ✅ 表单简单,复用现有逻辑
652 -- ✅ 无需后端改动
653 -- ✅ 用户可灵活组合不同方案
654 -
655 -**缺点**
656 -- ❌ 每个方案单独生成,无法直接对比
657 -- ❌ 用户需要多次操作
658 -
659 -**用户流程**
660 -```
661 -用户填写方案一 → 提交 → 生成计划书1(方案一)
662 -
663 -用户点击"添加方案" → 填写方案二 → 提交 → 生成计划书2(方案二)
664 -
665 -用户点击"添加方案" → 填写方案三 → 提交 → 生成计划书3(方案三)
666 -
667 -计划书列表展示 3 个方案,用户可选择查看/分享
668 -```
669 -
670 -#### 方案 C:混合方案(最佳用户体验)
671 -
672 -**第一阶段**:快速实现
673 -- 支持多次提交(方案B)
674 -- 在计划书列表页添加"同一客户的多个方案"分组展示
675 -
676 -**第二阶段**:完整方案
677 -- 支持单次提交多方案(方案A)
678 -- 计划书生成后可展示方案对比表格
679 -
680 -### 实施建议
681 -
682 -#### 短期(MVP)
683 -1. **实现多阶段提取**(本文档原有方案)
684 -2. **支持多次提交不同方案**
685 - - 在计划书列表中按客户/产品分组
686 - - 允许用户快速复制已有方案创建新方案
687 -
688 -#### 中期(完整多方案)
689 -1. **改造表单支持多方案**
690 - - 添加"方案管理"组件
691 - - 支持添加/删除/切换方案
692 -2. **后端支持多方案数据结构**
693 - - 新增 `withdrawal_scenarios_json` 字段
694 -3. **计划书生成支持方案对比**
695 -
696 -#### 技术架构调整
697 -
698 -```javascript
699 -// 数据结构扩展
700 -formData = {
701 - // ... 基础字段
702 -
703 - // 新增:方案模式
704 - scenario_mode: 'single' | 'multiple', // 单方案 | 多方案
705 -
706 - // 单方案模式(现有逻辑复用)
707 - withdrawal_enabled: '是',
708 - withdrawal_mode: '单阶段定期提取' | '多阶段分时提取',
709 - // ... 单方案字段
710 -
711 - // 多方案模式(新增)
712 - withdrawal_scenarios: [
713 - {
714 - scenario_id: Date.now(),
715 - scenario_name: '方案一',
716 - withdrawal_mode: '多阶段分时提取',
717 - withdrawal_stages: [...]
718 - },
719 - {
720 - scenario_id: Date.now() + 1,
721 - scenario_name: '方案二',
722 - withdrawal_mode: '单阶段定期提取',
723 - // ... 单方案字段
724 - }
725 - ]
726 -}
727 -```
728 -
729 ----
730 -
731 ## 待确认事项 533 ## 待确认事项
732 534
733 ### 与客户确认 535 ### 与客户确认
734 536
735 #### 多阶段提取功能 537 #### 多阶段提取功能
736 -- [ ] 确认是否需要"多阶段分时提取"功能 538 +- [x] 确认需要"多阶段分时提取"功能 ✅
539 +- [x] 确认一份计划书只有一个提取方案 ✅
737 - [ ] 确认最多支持多少个阶段(设计方案:1-5个) 540 - [ ] 确认最多支持多少个阶段(设计方案:1-5个)
738 - [ ] 确认预设方案是否满足需求(教育+创业+退休、退休年金、财富传承) 541 - [ ] 确认预设方案是否满足需求(教育+创业+退休、退休年金、财富传承)
739 - [ ] 确认后端是否能够接收 `withdrawal_stages_json` 字段 542 - [ ] 确认后端是否能够接收 `withdrawal_stages_json` 字段
740 - [ ] 确认多阶段提取的计划书生成方式 543 - [ ] 确认多阶段提取的计划书生成方式
741 544
742 -#### 多方案功能(重要)
743 -- [ ] **确认是否需要在一个计划书中展示多个提取方案**
744 -- [ ] 确认方案数量上限(客户资料中显示3个方案)
745 -- [ ] 确认实施方案:
746 - - **方案A**:单次提交多方案(需要后端改动)
747 - - **方案B**:多次提交不同方案(快速实现,无需后端改动)
748 - - **方案C**:混合方案(先B后A)
749 -- [ ] 确认后端是否能够接收 `withdrawal_scenarios_json` 字段
750 -
751 ### 技术细节 545 ### 技术细节
752 546
753 - [ ] 确认一笔过提取时的年龄表示方式(start_age = end_age) 547 - [ ] 确认一笔过提取时的年龄表示方式(start_age = end_age)
754 - [ ] 确认金额单位(前端:分,后端:元) 548 - [ ] 确认金额单位(前端:分,后端:元)
755 - [ ] 确认阶段数量上限(设计方案:5个) 549 - [ ] 确认阶段数量上限(设计方案:5个)
756 -- [ ] 确认多方案UI交互设计(Tab切换?卡片列表?)
757 550
758 --- 551 ---
759 552
760 ## 附录 553 ## 附录
761 554
762 -### 客户文档提取方案参考 555 +### 多阶段提取方案示例
763 556
764 -**方案一(3岁小公主)** 557 +**示例:教育+创业+退休组合**
765 -```
766 -18-21岁:每年 5万(教育基金)
767 -30岁:一笔过 40万(创业金/婚嫁金)← "一笔过" = 一次性提取
768 -50-100岁:每年 7万(退休养老年金)
769 ``` 558 ```
559 +阶段1 - 教育基金:
560 + 18-21岁,每年提取 5万
770 561
771 -> **术语说明**:"一笔过"是客户使用的术语,等同于"一次性提取",即在特定年龄一次性全额提取保单价值,而非分期提取。 562 +阶段2 - 创业金/婚嫁金:
563 + 30岁,一笔过提取 40万
772 564
773 -**方案二(37岁)** 565 +阶段3 - 退休养老年金:
774 -``` 566 + 50-100岁,每年提取 7万
775 -43-100岁:每年 3.75万(退休年金)
776 ``` 567 ```
777 568
778 -**方案三(财富传承)** 569 +> **术语说明**:"一笔过"是客户使用的术语,等同于"一次性提取",即在特定年龄一次性全额提取保单价值,而非分期提取。
779 -```
780 -不填写提取计划,展示保单价值增长表
781 -```
782 570
783 ### 现有系统架构参考 571 ### 现有系统架构参考
784 572
......
1 +# 客户新需求记录(2026-02-25)
2 +
3 +> **创建时间**: 2026-02-25
4 +> **状态**: ⏳ 待确认方案
5 +> **优先级**: 中
6 +
7 +---
8 +
9 +## 📋 需求清单
10 +
11 +### 需求 1:重疾类产品缴费年期新增选项
12 +
13 +**描述**:重疾类产品申请时,模板内"缴费年期"增加"直至65岁"选项
14 +
15 +**影响产品**
16 +- ✅ MPC 守护无间重疾(已有产品)
17 +- ✅ MBC PRO 活跃人生重疾保 PRO(已有产品)
18 +- ✅ MBC2 活跃人生重疾保 2(已有产品)
19 +
20 +**当前状态**
21 +- 现有缴费年期选项:10年、20年、25年
22 +- 需要新增:直至65岁
23 +
24 +**技术实现**
25 +- 文件:`src/config/plan-templates.js`
26 +- 修改三款重疾产品的 `payment_periods` 配置
27 +
28 +---
29 +
30 +### 需求 2:年龄与出生年月日二选一填写
31 +
32 +**描述**:所有产品申请时,年龄与出生年月日改为二选一进行填写
33 +
34 +**影响范围**
35 +- ✅ 所有产品类型(人寿、重疾、储蓄)
36 +
37 +**当前状态**
38 +- 现有:必填出生年月日,年龄自动计算
39 +- 需求:二选一填写(填年龄自动计算生日,或填生日自动计算年龄)
40 +
41 +**UI 交互设计**
42 +```
43 +┌─────────────────────────────────┐
44 +│ 出生日期(二选一) │
45 +├─────────────────────────────────┤
46 +│ ○ 填写年龄 │
47 +│ 年龄: [ 30 ] 岁 │
48 +│ │
49 +│ ○ 填写出生年月日 │
50 +│ 生日: [ 1990-01-01 ] │
51 +└─────────────────────────────────┘
52 +```
53 +
54 +**技术实现**
55 +- 新增表单字段:`age_input_mode`
56 +- 修改 Schema:条件显示年龄或生日输入
57 +- 添加自动计算逻辑
58 +
59 +---
60 +
61 +### 需求 3:储蓄类产品多阶段提取方案
62 +
63 +**描述**:储蓄类产品的提取方案改为可设置多阶段
64 +
65 +**客户确认**
66 +- ✅ 一份计划书里只有一个提取方案,不会有多个
67 +- ✅ 需要支持在一个方案中设置多个不同阶段的提取计划
68 +
69 +**示例**(教育+创业+退休组合):
70 +```
71 +┌──────────────────────────────────────────┐
72 +│ 方案一:多阶段分时提取 │
73 +├──────────────────────────────────────────┤
74 +│ 阶段1: 教育基金 │
75 +│ 18-21岁,每年提取 5万 │
76 +│ │
77 +│ 阶段2: 创业金/婚嫁金 │
78 +│ 30岁,一笔过提取 40万 │
79 +│ │
80 +│ 阶段3: 退休养老年金 │
81 +│ 50-100岁,每年提取 7万 │
82 +└──────────────────────────────────────────┘
83 +```
84 +
85 +**设计文档**:参见本目录下的 `多阶段提取方案设计.md`
86 +
87 +---
88 +
89 +## 📊 实施计划
90 +
91 +### 优先级排序
92 +
93 +| 优先级 | 需求 | 预估工时 | 依赖 |
94 +|--------|------|----------|------|
95 +| **P1** | 需求1:重疾缴费年期 | 30分钟 | 无 |
96 +| **P2** | 需求2:年龄生日二选一 | 2-3小时 | 无 |
97 +| **P3** | 需求3:多阶段提取 | 4-5小时 | 需求2 |
98 +
99 +### 实施步骤
100 +
101 +#### 第 1 步:重疾缴费年期新增选项(30分钟)
102 +
103 +**文件**`src/config/plan-templates.js`
104 +
105 +**修改内容**
106 +```javascript
107 +// 为三款重疾产品的 payment_periods 添加 "直至65岁" 选项
108 +payment_periods: [
109 + '10 年(15 日 - 65 岁)',
110 + '20 年(15 日 - 65 岁)',
111 + '25 年(15 日 - 60 岁)',
112 + '直至65岁' // 新增
113 +]
114 +```
115 +
116 +---
117 +
118 +#### 第 2 步:年龄与出生年月日二选一(2-3小时)
119 +
120 +**涉及文件**
121 +1. `src/config/plan-templates.js` - Schema 配置
122 +2. `src/components/plan/PlanFields/` - 新增组件
123 +3. `src/components/plan/PlanTemplates/` - 模板组件逻辑
124 +
125 +**Schema 扩展**
126 +```javascript
127 +base_fields: [
128 + // 新增:年龄输入模式选择
129 + {
130 + id: 'age_input_mode',
131 + key: 'age_input_mode',
132 + type: 'radio',
133 + label: '请选择输入方式',
134 + options: ['填写年龄', '填写出生年月日'],
135 + required: true,
136 + default: '填写出生年月日'
137 + },
138 + // 年龄输入(条件显示)
139 + {
140 + id: 'age',
141 + key: 'age',
142 + type: 'age',
143 + label: '年龄',
144 + placeholder: '请输入年龄',
145 + required: true,
146 + show_when: { field: 'age_input_mode', op: 'eq', value: '填写年龄' },
147 + clear_when_hidden: true
148 + },
149 + // 出生年月日输入(条件显示)
150 + {
151 + id: 'birthday',
152 + key: 'birthday',
153 + type: 'date',
154 + label: '出生年月日',
155 + placeholder: '请选择年月日',
156 + required: true,
157 + show_when: { field: 'age_input_mode', op: 'eq', value: '填写出生年月日' },
158 + clear_when_hidden: true
159 + }
160 +]
161 +```
162 +
163 +**自动计算逻辑**
164 +- 填写年龄 → 自动计算出生年月日(默认当年生日)
165 +- 填写出生年月日 → 自动计算年龄(当前年份 - 出生年份)
166 +
167 +---
168 +
169 +#### 第 3 步:多阶段提取方案(4-5小时)
170 +
171 +详见 `多阶段提取方案设计.md`
172 +
173 +---
174 +
175 +## 📝 待确认事项
176 +
177 +### 客户确认
178 +
179 +- [ ] **需求1确认**
180 + - [ ] 确认"直至65岁"选项文字表述
181 + - [ ] 确认是否需要年龄限制(如:最大投保年龄)
182 +
183 +- [ ] **需求2确认**
184 + - [ ] 确认默认选项(建议:填写出生年月日)
185 + - [ ] 确认填写年龄时的默认生日(建议:当年生日)
186 +
187 +- [ ] **需求3确认**
188 + - [ ] 确认阶段数量上限(设计方案:1-5个)
189 + - [ ] 确认预设方案模板(教育+创业+退休、退休年金、财富传承)
190 +
191 +### 技术确认
192 +
193 +- [ ] 后端能否接收新增字段:
194 + - `age_input_mode`
195 + - `withdrawal_stages_json`
196 +- [ ] 后端年龄计算逻辑是否与前端一致
197 +
198 +---
199 +
200 +## 🔗 相关文档
201 +
202 +- [多阶段提取方案设计](./多阶段提取方案设计.md)
203 +- [计划书架构文档](../../plan/plan-entry-architecture.md)
204 +- [Schema 配置规范](../../plan/plan-config-schema-reference.md)
205 +
206 +---
207 +
208 +**文档版本**: 1.0.0
209 +**最后更新**: 2026-02-25
1 -/**
2 - * 热卖产品 Mock 数据
3 - *
4 - * @description 包含项目所有保险类型的产品 mock 数据,用于测试计划书模板显示
5 - * 遵循项目 mock 数据规范,集成到 src/utils/mockData.js
6 - * @module api/mock/hotProducts
7 - * @author Claude Code
8 - * @created 2026-02-09
9 - */
10 -
11 -/**
12 - * 热卖产品 Mock 数据列表
13 - * @description 包含 9 种产品,覆盖所有计划书模板类型
14 - */
15 -const HOT_PRODUCTS = [
16 - // ====== 人寿保险 (LifeInsuranceTemplate) ======
17 -
18 - {
19 - id: 1,
20 - product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
21 - name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
22 - recommend: 'hot',
23 - form_sn: 'life-insurance-wiop3e', // 对应 LifeInsuranceTemplate
24 - categories: [
25 - { id: 'life', name: '人寿保险' }
26 - ],
27 - tags: [
28 - { id: 't1', name: '终身寿险', bg_color: '#DBEAFE', text_color: '#1E40AF' },
29 - { id: 't2', name: '美元产品', bg_color: '#DCFCE7', text_color: '#166534' }
30 - ],
31 - cover_image: 'https://picsum.photos/seed/wiop3e/400/300'
32 - },
33 -
34 - {
35 - id: 2,
36 - product_name: 'WIOP3 盈传创富保障计划 3',
37 - name: 'WIOP3 盈传创富保障计划 3',
38 - recommend: 'hot',
39 - form_sn: 'life-insurance-wiop3', // 对应 LifeInsuranceTemplate
40 - categories: [
41 - { id: 'life', name: '人寿保险' }
42 - ],
43 - tags: [
44 - { id: 't1', name: '终身寿险', bg_color: '#DBEAFE', text_color: '#1E40AF' },
45 - { id: 't2', name: '美元产品', bg_color: '#DCFCE7', text_color: '#166534' }
46 - ],
47 - cover_image: 'https://picsum.photos/seed/wiop3/400/300'
48 - },
49 -
50 - // ====== 重疾保险 (CriticalIllnessTemplate) ======
51 -
52 - {
53 - id: 3,
54 - product_name: 'MPC 守护无间重疾',
55 - name: 'MPC 守护无间重疾',
56 - recommend: 'hot',
57 - form_sn: 'critical-illness-mpc', // 对应 CriticalIllnessTemplate
58 - categories: [
59 - { id: 'critical', name: '重疾保险' }
60 - ],
61 - tags: [
62 - { id: 't1', name: '重疾保障', bg_color: '#FEF3C7', text_color: '#92400E' },
63 - { id: 't2', name: '终身保障', bg_color: '#E0E7FF', text_color: '#3730A3' }
64 - ],
65 - cover_image: 'https://picsum.photos/seed/mpc/400/300'
66 - },
67 -
68 - {
69 - id: 4,
70 - product_name: 'MBC PRO 活跃人生重疾保 PRO',
71 - name: 'MBC PRO 活跃人生重疾保 PRO',
72 - recommend: 'hot',
73 - form_sn: 'critical-illness-mbc-pro', // 对应 CriticalIllnessTemplate
74 - categories: [
75 - { id: 'critical', name: '重疾保险' }
76 - ],
77 - tags: [
78 - { id: 't1', name: '重疾保障', bg_color: '#FEF3C7', text_color: '#92400E' },
79 - { id: 't2', name: 'PRO 版本', bg_color: '#FEE2E2', text_color: '#991B1B' }
80 - ],
81 - cover_image: 'https://picsum.photos/seed/mbc-pro/400/300'
82 - },
83 -
84 - {
85 - id: 5,
86 - product_name: 'MBC2 活跃人生重疾保 2',
87 - name: 'MBC2 活跃人生重疾保 2',
88 - recommend: 'hot',
89 - form_sn: 'critical-illness-mbc2', // 对应 CriticalIllnessTemplate
90 - categories: [
91 - { id: 'critical', name: '重疾保险' }
92 - ],
93 - tags: [
94 - { id: 't1', name: '重疾保障', bg_color: '#FEF3C7', text_color: '#92400E' },
95 - { id: 't2', name: '升级版', bg_color: '#DCFCE7', text_color: '#166534' }
96 - ],
97 - cover_image: 'https://picsum.photos/seed/mbc2/400/300'
98 - },
99 -
100 - // ====== 储蓄型产品 (SavingsTemplate) ======
101 -
102 - {
103 - id: 6,
104 - product_name: 'GS 宏挚传承保障计划',
105 - name: 'GS 宏挚传承保障计划',
106 - recommend: 'hot',
107 - form_sn: 'savings-gs', // 对应 SavingsTemplate
108 - categories: [
109 - { id: 'savings', name: '储蓄保险' }
110 - ],
111 - tags: [
112 - { id: 't1', name: '储蓄型', bg_color: '#E0E7FF', text_color: '#3730A3' },
113 - { id: 't2', name: '传承规划', bg_color: '#F3E8FF', text_color: '#6B21A8' }
114 - ],
115 - cover_image: 'https://picsum.photos/seed/gs/400/300'
116 - },
117 -
118 - {
119 - id: 7,
120 - product_name: 'GC 宏挚家传保险计划',
121 - name: 'GC 宏挚家传保险计划',
122 - recommend: 'hot',
123 - form_sn: 'savings-gc', // 对应 SavingsTemplate
124 - categories: [
125 - { id: 'savings', name: '储蓄保险' }
126 - ],
127 - tags: [
128 - { id: 't1', name: '储蓄型', bg_color: '#E0E7FF', text_color: '#3730A3' },
129 - { id: 't2', name: '家庭保障', bg_color: '#FEE2E2', text_color: '#991B1B' }
130 - ],
131 - cover_image: 'https://picsum.photos/seed/gc/400/300'
132 - },
133 -
134 - {
135 - id: 8,
136 - product_name: 'FA 宏浚传承保障计划',
137 - name: 'FA 宏浚传承保障计划',
138 - recommend: 'hot',
139 - form_sn: 'savings-fa', // 对应 SavingsTemplate
140 - categories: [
141 - { id: 'savings', name: '储蓄保险' }
142 - ],
143 - tags: [
144 - { id: 't1', name: '储蓄型', bg_color: '#E0E7FF', text_color: '#3730A3' },
145 - { id: 't2', name: '财富传承', bg_color: '#FEF3C7', text_color: '#92400E' }
146 - ],
147 - cover_image: 'https://picsum.photos/seed/fa/400/300'
148 - },
149 -
150 - {
151 - id: 9,
152 - product_name: 'LV2 赤霞珠终身寿险计划2',
153 - name: 'LV2 赤霞珠终身寿险计划2',
154 - recommend: 'hot',
155 - form_sn: 'savings-lv2', // 对应 SavingsTemplate
156 - categories: [
157 - { id: 'savings', name: '储蓄保险' }
158 - ],
159 - tags: [
160 - { id: 't1', name: '储蓄型', bg_color: '#E0E7FF', text_color: '#3730A3' },
161 - { id: 't2', name: '终身寿险', bg_color: '#DBEAFE', text_color: '#1E40AF' }
162 - ],
163 - cover_image: 'https://picsum.photos/seed/lv2/400/300'
164 - }
165 -]
166 -
167 -/**
168 - * Mock: listAPI (热卖产品)
169 - * @description 专门用于首页热卖产品的 Mock API,支持 form_sn 字段
170 - * @param {Object} params - 请求参数
171 - * @param {string} params.recommend - 推荐位(必须为 'hot')
172 - * @returns {Promise<Object>} 模拟的 API 响应
173 - */
174 -export async function mockHotProductsListAPI(params) {
175 - const { recommend } = params
176 -
177 - // 只返回热卖产品
178 - if (recommend !== 'hot') {
179 - return { code: 0, msg: '只支持热卖产品查询', data: { list: [], total: 0 } }
180 - }
181 -
182 - // 模拟网络延迟
183 - await new Promise(resolve => setTimeout(resolve, 200))
184 -
185 - console.log('[Mock] hotProductsListAPI - 返回热卖产品', HOT_PRODUCTS.length, '条')
186 -
187 - return {
188 - code: 1,
189 - msg: 'success',
190 - data: {
191 - list: HOT_PRODUCTS,
192 - total: HOT_PRODUCTS.length,
193 - categories: [
194 - { id: 'life', name: '人寿保险' },
195 - { id: 'critical', name: '重疾保险' },
196 - { id: 'savings', name: '储蓄保险' }
197 - ]
198 - }
199 - }
200 -}
201 -
202 -/**
203 - * 导出 Mock API 函数供其他模块使用
204 - */
205 -export default {
206 - mockHotProductsListAPI
207 -}
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
11 /** 11 /**
12 * 热卖产品 Mock 数据列表 12 * 热卖产品 Mock 数据列表
13 * 13 *
14 - * @description 包含 9 种产品,覆盖所有计划书模板类型: 14 + * @description 包含 10 种产品,覆盖所有计划书模板类型:
15 * - 人寿保险 (2 种): WIOP3E、WIOP3 15 * - 人寿保险 (2 种): WIOP3E、WIOP3
16 * - 重疾保险 (3 种): MPC、MBC PRO、MBC2 16 * - 重疾保险 (3 种): MPC、MBC PRO、MBC2
17 - * - 储蓄型产品 (4 种): GS、GC、FA、LV2 17 + * - 储蓄型产品 (5 种): GS、GC、FA、LV2、GS(多阶段)
18 */ 18 */
19 export const hotProductsMockData = [ 19 export const hotProductsMockData = [
20 // ====== 人寿保险 (LifeInsuranceTemplate) ====== 20 // ====== 人寿保险 (LifeInsuranceTemplate) ======
...@@ -165,6 +165,25 @@ export const hotProductsMockData = [ ...@@ -165,6 +165,25 @@ export const hotProductsMockData = [
165 ], 165 ],
166 cover_image: 'https://picsum.photos/seed/lv2/400/300', 166 cover_image: 'https://picsum.photos/seed/lv2/400/300',
167 created_time: '2026-01-09 00:00:00' 167 created_time: '2026-01-09 00:00:00'
168 + },
169 +
170 + // ====== 储蓄型产品 - 多阶段提取(新增 2026-02-25) ======
171 +
172 + {
173 + id: 10,
174 + product_name: 'GS 宏挚传承保障计划(多阶段)',
175 + recommend: 'hot',
176 + form_sn: 'savings-gs-multistage', // 对应 SavingsTemplate 多阶段模式
177 + categories: [
178 + { id: 'savings', name: '储蓄保险' }
179 + ],
180 + tags: [
181 + { id: 't1', name: '多阶段提取', bg_color: '#F3E8FF', text_color: '#6B21A8' },
182 + { id: 't2', name: '新品', bg_color: '#DCFCE7', text_color: '#166534' },
183 + { id: 't3', name: '灵活规划', bg_color: '#FEF3C7', text_color: '#92400E' }
184 + ],
185 + cover_image: 'https://picsum.photos/seed/gs-multistage/400/300',
186 + created_time: '2026-02-25 00:00:00'
168 } 187 }
169 ] 188 ]
170 189
...@@ -288,6 +307,7 @@ export function getAllTemplateTypes() { ...@@ -288,6 +307,7 @@ export function getAllTemplateTypes() {
288 * - ✅ savings-gc - GC 宏挚家传保险计划 307 * - ✅ savings-gc - GC 宏挚家传保险计划
289 * - ✅ savings-fa - FA 宏浚传承保障计划 308 * - ✅ savings-fa - FA 宏浚传承保障计划
290 * - ✅ savings-lv2 - LV2 赤霞珠终身寿险计划2 309 * - ✅ savings-lv2 - LV2 赤霞珠终身寿险计划2
310 + * - ✅ savings-gs-multistage - GS 宏挚传承保障计划(多阶段) - 支持多阶段提取
291 * 311 *
292 * ## 测试步骤 312 * ## 测试步骤
293 * 313 *
......
...@@ -25,7 +25,92 @@ ...@@ -25,7 +25,92 @@
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 - <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> 28 + <!-- 多阶段提取计划(优先显示) -->
29 + <div v-if="isMultiStageMode" class="multi-stage-withdrawal-section">
30 + <h3 class="text-base font-semibold text-gray-900 mb-4">
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">
29 <template v-for="field in withdrawalFields" :key="field.id || field.key"> 114 <template v-for="field in withdrawalFields" :key="field.id || field.key">
30 <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4"> 115 <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4">
31 {{ field.section_title }} 116 {{ field.section_title }}
...@@ -75,7 +160,7 @@ ...@@ -75,7 +160,7 @@
75 * :config="templateConfig" 160 * :config="templateConfig"
76 * /> 161 * />
77 */ 162 */
78 -import { reactive, watch, computed } from 'vue' 163 +import { reactive, watch, computed, ref } from 'vue'
79 import Taro from '@tarojs/taro' 164 import Taro from '@tarojs/taro'
80 import PlanFieldName from '../PlanFields/NameInput.vue' 165 import PlanFieldName from '../PlanFields/NameInput.vue'
81 import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' 166 import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
...@@ -224,6 +309,158 @@ const getFieldProps = (field) => { ...@@ -224,6 +309,158 @@ const getFieldProps = (field) => {
224 309
225 const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions) 310 const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
226 311
312 +// ====== 多阶段提取计划逻辑 ======
313 +
314 +/**
315 + * 是否为多阶段模式
316 + * @description 检查产品配置是否启用了 multi_stage_withdrawal
317 + */
318 +const isMultiStageMode = computed(() => {
319 + return props.config?.multi_stage_withdrawal?.enabled === true
320 +})
321 +
322 +/**
323 + * 多阶段配置对象(便捷访问)
324 + */
325 +const multiStageConfig = computed(() => {
326 + return props.config?.multi_stage_withdrawal || {}
327 +})
328 +
329 +/**
330 + * 多阶段提取期选项(包含"一笔过")
331 + */
332 +const multiStagePeriodOptions = computed(() => {
333 + return multiStageConfig.value.withdrawal_periods ||
334 + props.config?.withdrawal_plan?.withdrawal_periods ||
335 + []
336 +})
337 +
338 +/**
339 + * 阶段数据数组
340 + * @description 每个阶段包含:annual_withdrawal_amount, withdrawal_start_age, withdrawal_period, annual_increase_percentage
341 + */
342 +const stages = ref([])
343 +
344 +/**
345 + * 是否可添加阶段
346 + * @description 年龄 ≥ 阈值且未达上限
347 + */
348 +const canAddStage = computed(() => {
349 + const age = parseInt(form.age) || 0
350 + const threshold = multiStageConfig.value.age_threshold || 12
351 + const limit = multiStageConfig.value.stage_limit || 4
352 + return age >= threshold && stages.value.length < limit
353 +})
354 +
355 +/**
356 + * 是否可删除阶段
357 + * @description 年龄 ≥ 阈值且至少有2个阶段
358 + */
359 +const canRemoveStage = computed(() => {
360 + const age = parseInt(form.age) || 0
361 + const threshold = multiStageConfig.value.age_threshold || 12
362 + return age >= threshold && stages.value.length > 1
363 +})
364 +
365 +/**
366 + * 创建空的阶段数据
367 + * @returns {Object} 空阶段对象
368 + */
369 +const createStage = () => ({
370 + annual_withdrawal_amount: null,
371 + withdrawal_start_age: null,
372 + withdrawal_period: null,
373 + annual_increase_percentage: null
374 +})
375 +
376 +/**
377 + * 初始化阶段数据
378 + * @description 根据年龄初始化阶段数量:
379 + * - 年龄 < 12岁:固定 3 组
380 + * - 年龄 ≥ 12岁:初始 1 组
381 + */
382 +const initializeStages = () => {
383 + const age = parseInt(form.age) || 0
384 + const threshold = multiStageConfig.value.age_threshold || 12
385 +
386 + if (age < threshold) {
387 + // 固定 3 组
388 + stages.value = [createStage(), createStage(), createStage()]
389 + } else {
390 + // 初始 1 组
391 + stages.value = [createStage()]
392 + }
393 +}
394 +
395 +/**
396 + * 添加新阶段
397 + * @description 在当前阶段列表末尾添加一个空阶段
398 + */
399 +const addStage = () => {
400 + if (!canAddStage.value) return
401 + stages.value.push(createStage())
402 +}
403 +
404 +/**
405 + * 删除指定阶段
406 + * @param {number} index - 要删除的阶段索引
407 + */
408 +const removeStage = (index) => {
409 + if (!canRemoveStage.value) return
410 + stages.value.splice(index, 1)
411 +}
412 +
413 +/**
414 + * 同步阶段数据到表单
415 + * @description 将 stages 数组同步到 form.withdrawal_stages,以便父组件获取
416 + * 同时清理 undefined 值为 null,确保提交数据格式正确
417 + */
418 +watch(
419 + stages,
420 + (newStages) => {
421 + // 清理每个阶段的 undefined 值为 null
422 + const cleanedStages = newStages.map(stage => ({
423 + annual_withdrawal_amount: stage.annual_withdrawal_amount ?? null,
424 + withdrawal_start_age: stage.withdrawal_start_age ?? null,
425 + withdrawal_period: stage.withdrawal_period ?? null,
426 + annual_increase_percentage: stage.annual_increase_percentage ?? null
427 + }))
428 + form.withdrawal_stages = cleanedStages
429 + },
430 + { deep: true }
431 +)
432 +
433 +// 监听年龄变化,重新初始化阶段(仅在多阶段模式下)
434 +watch(
435 + () => form.age,
436 + (newAge, oldAge) => {
437 + if (isMultiStageMode.value && newAge !== oldAge && newAge !== undefined) {
438 + const threshold = multiStageConfig.value.age_threshold || 12
439 + const newAgeInt = parseInt(newAge) || 0
440 + const oldAgeInt = parseInt(oldAge) || 0
441 +
442 + // 跨越阈值时重新初始化
443 + if ((newAgeInt < threshold && oldAgeInt >= threshold) ||
444 + (newAgeInt >= threshold && oldAgeInt < threshold)) {
445 + initializeStages()
446 + }
447 + }
448 + }
449 +)
450 +
451 +// 组件挂载时初始化阶段(多阶段模式)
452 +watch(
453 + isMultiStageMode,
454 + (enabled) => {
455 + if (enabled && stages.value.length === 0) {
456 + initializeStages()
457 + }
458 + },
459 + { immediate: true }
460 +)
461 +
462 +// ====== 原有表单逻辑 ======
463 +
227 /** 464 /**
228 * 获取 Schema 默认值 465 * 获取 Schema 默认值
229 * @param {Object} value - 当前表单数据 466 * @param {Object} value - 当前表单数据
...@@ -371,7 +608,7 @@ watch( ...@@ -371,7 +608,7 @@ watch(
371 /** 608 /**
372 * 百分比输入清洗,避免非法字符 609 * 百分比输入清洗,避免非法字符
373 * @param {string|number} value - 输入值 610 * @param {string|number} value - 输入值
374 - * @param {string} key - 目标字段 key 611 + * @param {string} key - 目标字段 key(支持多阶段路径:stages.${index}.annual_increase_percentage)
375 */ 612 */
376 const onPercentageInput = (value, key) => { 613 const onPercentageInput = (value, key) => {
377 // 转换为字符串(处理 value 为 null 或其他类型的情况) 614 // 转换为字符串(处理 value 为 null 或其他类型的情况)
...@@ -401,7 +638,17 @@ const onPercentageInput = (value, key) => { ...@@ -401,7 +638,17 @@ const onPercentageInput = (value, key) => {
401 } 638 }
402 } 639 }
403 640
404 - form[key] = cleaned 641 + // 处理多阶段路径(如 stages.0.annual_increase_percentage)
642 + if (key.startsWith('stages.')) {
643 + const pathParts = key.split('.')
644 + const stageIndex = parseInt(pathParts[1])
645 + const fieldKey = pathParts[2]
646 + if (!Number.isNaN(stageIndex) && stages.value[stageIndex]) {
647 + stages.value[stageIndex][fieldKey] = cleaned
648 + }
649 + } else {
650 + form[key] = cleaned
651 + }
405 } 652 }
406 653
407 const isEmptyValue = (value) => { 654 const isEmptyValue = (value) => {
...@@ -434,6 +681,12 @@ const isFieldRequired = (field) => { ...@@ -434,6 +681,12 @@ const isFieldRequired = (field) => {
434 * @returns {boolean} 校验是否通过 681 * @returns {boolean} 校验是否通过
435 */ 682 */
436 const validate = () => { 683 const validate = () => {
684 + // 多阶段模式校验
685 + if (isMultiStageMode.value) {
686 + return validateMultiStage()
687 + }
688 +
689 + // 单阶段模式校验(原有逻辑)
437 const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] 690 const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])]
438 691
439 // 年龄与出生年月日二选一校验 692 // 年龄与出生年月日二选一校验
...@@ -484,6 +737,77 @@ const validate = () => { ...@@ -484,6 +737,77 @@ const validate = () => {
484 } 737 }
485 738
486 /** 739 /**
740 + * 多阶段表单校验
741 + * @description 校验每个阶段的必填字段
742 + * @returns {boolean} 校验是否通过
743 + */
744 +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 + // 多阶段字段校验
775 + for (let i = 0; i < stages.value.length; i++) {
776 + const stage = stages.value[i]
777 + const stageLabel = `阶段${i + 1}`
778 +
779 + // 每年提取金额(必填)
780 + if (isEmptyValue(stage.annual_withdrawal_amount)) {
781 + Taro.showToast({ title: `${stageLabel}:请输入每年提取金额`, icon: 'none' })
782 + return false
783 + }
784 +
785 + // 由几岁开始(必填)
786 + if (isEmptyValue(stage.withdrawal_start_age)) {
787 + Taro.showToast({ title: `${stageLabel}:请输入由几岁开始`, icon: 'none' })
788 + return false
789 + }
790 +
791 + // 提取期(必填)
792 + if (isEmptyValue(stage.withdrawal_period)) {
793 + Taro.showToast({ title: `${stageLabel}:请选择提取期`, icon: 'none' })
794 + return false
795 + }
796 +
797 + // 每年递增提取之百分比(可选,校验范围)
798 + if (!isEmptyValue(stage.annual_increase_percentage)) {
799 + const percentage = parseFloat(stage.annual_increase_percentage)
800 + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
801 + Taro.showToast({ title: `${stageLabel}:递增百分比请输入0-100之间的数值`, icon: 'none' })
802 + return false
803 + }
804 + }
805 + }
806 +
807 + return true
808 +}
809 +
810 +/**
487 * 清除验证错误 811 * 清除验证错误
488 * @description 由于使用 Toast 显示错误,无需清除状态 812 * @description 由于使用 Toast 显示错误,无需清除状态
489 * 保留此方法以保持接口一致性 813 * 保留此方法以保持接口一致性
...@@ -506,4 +830,30 @@ defineExpose({ ...@@ -506,4 +830,30 @@ defineExpose({
506 padding-left: 20rpx !important; 830 padding-left: 20rpx !important;
507 } 831 }
508 } 832 }
833 +
834 +/* 多阶段提取计划样式 */
835 +.multi-stage-withdrawal-section {
836 + .stage-card {
837 + background: #ffffff;
838 + border-radius: 12rpx;
839 + padding: 32rpx;
840 + margin-bottom: 32rpx;
841 + border: 1rpx solid #e5e7eb;
842 + }
843 +
844 + .stage-title {
845 + font-size: 28rpx;
846 + font-weight: 600;
847 + color: #111827;
848 + }
849 +
850 + .percentage-field {
851 + margin-top: 24rpx;
852 + }
853 +
854 + .add-stage-btn {
855 + margin-top: 24rpx;
856 + border-radius: 12rpx;
857 + }
858 +}
509 </style> 859 </style>
......
...@@ -111,6 +111,23 @@ const savingsFormSchema = { ...@@ -111,6 +111,23 @@ const savingsFormSchema = {
111 // 当 withdrawal_mode 切换时,不可见的字段会自动清空 111 // 当 withdrawal_mode 切换时,不可见的字段会自动清空
112 } 112 }
113 113
114 +/**
115 + * 多阶段提取计划配置
116 + * @description 用于"宏挚传承保障计划(多阶段)"等支持多阶段提取的产品
117 + * @updated 2026-02-25 - 新增多阶段提取功能
118 + */
119 +const multiStageWithdrawalConfig = {
120 + enabled: true,
121 + stage_limit: 4, // 阶段上限(预留修改空间)
122 + age_threshold: 12, // 年龄阈值(<12岁固定3组,≥12岁可添加)
123 + withdrawal_periods: [
124 + '1年', '2年', '3年', '5年',
125 + '10年', '15年', '20年', '终身',
126 + '一笔过' // 新增:一次性提取选项
127 + ],
128 + percentage_optional: true // 递增百分比可选
129 +}
130 +
114 export const PLAN_TEMPLATES = { 131 export const PLAN_TEMPLATES = {
115 // 人寿保险产品 - WIOP3E 132 // 人寿保险产品 - WIOP3E
116 'life-insurance-wiop3e': { 133 'life-insurance-wiop3e': {
...@@ -208,7 +225,7 @@ export const PLAN_TEMPLATES = { ...@@ -208,7 +225,7 @@ export const PLAN_TEMPLATES = {
208 225
209 // ====== 储蓄型产品(统一逻辑) ====== 226 // ====== 储蓄型产品(统一逻辑) ======
210 227
211 - // GS - 宏挚传承保障计划 228 + // GS - 宏挚传承保障计划(普通模式)
212 'savings-gs': { 229 'savings-gs': {
213 name: '宏挚传承保障计划', 230 name: '宏挚传承保障计划',
214 component: 'SavingsTemplate', 231 component: 'SavingsTemplate',
...@@ -351,6 +368,38 @@ export const PLAN_TEMPLATES = { ...@@ -351,6 +368,38 @@ export const PLAN_TEMPLATES = {
351 submit_mapping: savingsSubmitMapping 368 submit_mapping: savingsSubmitMapping
352 } 369 }
353 }, 370 },
371 +
372 + // GS - 宏挚传承保障计划(多阶段模式)⭐ 新增
373 + // @description 支持多阶段提取计划,根据年龄控制阶段数量
374 + // @updated 2026-02-25 - 新增多阶段提取功能
375 + 'savings-gs-multistage': {
376 + name: '宏挚传承保障计划(多阶段)',
377 + component: 'SavingsTemplate',
378 + category: 'savings',
379 + config: {
380 + currency: 'USD',
381 + payment_periods: [
382 + '整付',
383 + '3 年',
384 + '5 年',
385 + '10 年',
386 + '15 年',
387 + ],
388 + age_range: { min: 0, max: 100 },
389 + insurance_period: '终身',
390 + // 多阶段提取计划配置
391 + multi_stage_withdrawal: multiStageWithdrawalConfig,
392 + withdrawal_plan: {
393 + enabled: true,
394 + currencies: ['HKD', 'USD', 'CNY'],
395 + default_currency: 'USD',
396 + withdrawal_modes: ['指定提取金额'], // 多阶段模式只支持指定提取金额
397 + withdrawal_periods: multiStageWithdrawalConfig.withdrawal_periods
398 + },
399 + form_schema: savingsFormSchema,
400 + submit_mapping: savingsSubmitMapping
401 + }
402 + },
354 } 403 }
355 404
356 /** 405 /**
......