feat(plan): 合并选项控制功能到 develop
- 完善计划书字段选项控制功能 - 优化表单交互体验 - 提取字段配置模板
Showing
44 changed files
with
2402 additions
and
4669 deletions
| 1 | -# 产品配置审核 - 计划书模版2.docx | ||
| 2 | - | ||
| 3 | -**解析时间**: 2026/2/15 10:20:30 | ||
| 4 | -**原始文件**: 计划书模版2.docx | ||
| 5 | -**数据来源**: docs/to-parse/计划书模版2.docx | ||
| 6 | - | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -## 📋 产品基本信息 | ||
| 10 | - | ||
| 11 | -| 字段 | 提取值 | 需要确认 | | ||
| 12 | -|------|--------|---------| | ||
| 13 | -| 产品名称 | 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日 | ✅ 请核对产品名称 | | ||
| 14 | -| 产品类型 | savings | ✅ 请确认产品类型 | | ||
| 15 | -| 币种 | USD | ✅ 请确认币种 | | ||
| 16 | -| form_sn | `savings-product-ef3dd50b` | ✅ 请确认 form_sn 唯一性 | | ||
| 17 | -| 缴费年期 | ["整付","3年","5年","10年","15年"] | ✅ 请确认缴费年期选项 | | ||
| 18 | -| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 | | ||
| 19 | -| 保险期间 | 终身 | ✅ 请确认保险期间 | | ||
| 20 | - | ||
| 21 | - | ||
| 22 | -### 💰 储蓄类产品特有字段 | ||
| 23 | - | ||
| 24 | -| 字段 | 提取值 | 需要确认 | | ||
| 25 | -|------|--------|---------| | ||
| 26 | -| 提取方式 | ["最高固定提取金额"] | ✅ 请确认提取方式 | | ||
| 27 | -| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 | | ||
| 28 | - | ||
| 29 | - | ||
| 30 | ---- | ||
| 31 | - | ||
| 32 | -## 🤖 智能字段提取报告 | ||
| 33 | - | ||
| 34 | -### 匹配统计 | ||
| 35 | - | ||
| 36 | -- ✅ 成功匹配: 4 字段 | ||
| 37 | -- ⚠️ 使用默认值: 3 字段 | ||
| 38 | -- ❌ 未匹配(需人工补充): 0 字段 | ||
| 39 | - | ||
| 40 | -### ✅ 已成功匹配的字段 | ||
| 41 | - | ||
| 42 | -- product_name | ||
| 43 | -- product_type | ||
| 44 | -- payment_periods | ||
| 45 | -- withdrawal_modes | ||
| 46 | - | ||
| 47 | - | ||
| 48 | -### ⚠️ 使用默认值的字段 | ||
| 49 | - | ||
| 50 | -- **currency**: 未找到字段 "currency",使用默认值: "USD" | ||
| 51 | -- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75} | ||
| 52 | -- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身" | ||
| 53 | - | ||
| 54 | - | ||
| 55 | - | ||
| 56 | - | ||
| 57 | ---- | ||
| 58 | - | ||
| 59 | -## 🧾 配置预览 | ||
| 60 | - | ||
| 61 | -```javascript | ||
| 62 | -{ | ||
| 63 | - "product_name": "宏摯傳承保障計劃 - 性別, 年齡, 出生年月日", | ||
| 64 | - "product_type": "savings", | ||
| 65 | - "currency": "USD", | ||
| 66 | - "form_sn": "savings-product-ef3dd50b", | ||
| 67 | - "payment_periods": [ | ||
| 68 | - "整付", | ||
| 69 | - "3年", | ||
| 70 | - "5年", | ||
| 71 | - "10年", | ||
| 72 | - "15年" | ||
| 73 | - ], | ||
| 74 | - "age_range": { | ||
| 75 | - "min": 0, | ||
| 76 | - "max": 75 | ||
| 77 | - }, | ||
| 78 | - "insurance_period": "终身", | ||
| 79 | - "is_savings": true, | ||
| 80 | - "withdrawal_modes": [ | ||
| 81 | - "最高固定提取金额" | ||
| 82 | - ], | ||
| 83 | - "withdrawal_periods": [ | ||
| 84 | - "1年", | ||
| 85 | - "3年", | ||
| 86 | - "5年", | ||
| 87 | - "10年" | ||
| 88 | - ] | ||
| 89 | -} | ||
| 90 | -``` | ||
| 91 | - | ||
| 92 | ---- | ||
| 93 | - | ||
| 94 | -## 📝 表单字段 (form_schema) | ||
| 95 | - | ||
| 96 | -```javascript | ||
| 97 | -{ | ||
| 98 | - "base_fields": [], | ||
| 99 | - "withdrawal_fields": [], | ||
| 100 | - "reset_map": {} | ||
| 101 | -} | ||
| 102 | -``` | ||
| 103 | - | ||
| 104 | ---- | ||
| 105 | - | ||
| 106 | -## 🔄 提交字段映射 (submit_mapping) | ||
| 107 | - | ||
| 108 | -```javascript | ||
| 109 | -{} | ||
| 110 | -``` | ||
| 111 | - | ||
| 112 | ---- | ||
| 113 | - | ||
| 114 | -## 🧩 生成配置片段 | ||
| 115 | - | ||
| 116 | -```javascript | ||
| 117 | -/** | ||
| 118 | - * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日 | ||
| 119 | - * @added 2026-02-15T02:20:30.982Z | ||
| 120 | - * @source docs/to-parse/计划书模版2.docx | ||
| 121 | - */ | ||
| 122 | - 'savings-product-ef3dd50b': { | ||
| 123 | - name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日', | ||
| 124 | - component: 'SavingsTemplate', | ||
| 125 | - category: 'savings', | ||
| 126 | - config: { | ||
| 127 | - currency: 'USD', | ||
| 128 | - payment_periods: ["整付","3年","5年","10年","15年"], | ||
| 129 | - age_range: { min: 0, max: 75 }, | ||
| 130 | - insurance_period: '终身', | ||
| 131 | - withdrawal_plan: { | ||
| 132 | - enabled: true, | ||
| 133 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 134 | - default_currency: 'USD', | ||
| 135 | - withdrawal_modes: ["最高固定提取金额"], | ||
| 136 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 137 | - }, | ||
| 138 | - form_schema: savingsFormSchema, | ||
| 139 | - submit_mapping: savingsSubmitMapping | ||
| 140 | - } | ||
| 141 | - } | ||
| 142 | -``` | ||
| 143 | - | ||
| 144 | ---- | ||
| 145 | - | ||
| 146 | -## ✅ 审核检查清单 | ||
| 147 | - | ||
| 148 | -### 基础信息 | ||
| 149 | -- [ ] 产品名称正确 | ||
| 150 | -- [ ] 产品类型正确(savings/critical-illness/life-insurance) | ||
| 151 | -- [ ] 币种正确(USD/CNY/HKD/EUR) | ||
| 152 | -- [ ] form_sn 唯一且符合命名规范 | ||
| 153 | - | ||
| 154 | -### 缴费与年龄 | ||
| 155 | -- [ ] 缴费年期选项完整且正确 | ||
| 156 | -- [ ] 年龄范围合理 | ||
| 157 | -- [ ] 保险期间正确 | ||
| 158 | - | ||
| 159 | -### 储蓄类特有(如适用) | ||
| 160 | -- [ ] 提取方式正确 | ||
| 161 | -- [ ] 提取期选项完整 | ||
| 162 | -- [ ] 表单字段定义完整 | ||
| 163 | -- [ ] 提交字段映射正确 | ||
| 164 | - | ||
| 165 | ---- | ||
| 166 | - | ||
| 167 | -## 📋 审核后操作 | ||
| 168 | - | ||
| 169 | -### 确认无误 | ||
| 170 | -```bash | ||
| 171 | -# 1. 移动到 approved 目录 | ||
| 172 | -mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md \ | ||
| 173 | - docs/parse-audit/approved/ | ||
| 174 | - | ||
| 175 | -# 2. 合并到正式配置 | ||
| 176 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 177 | - | ||
| 178 | -# 3. 删除待审核文件(可选) | ||
| 179 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md | ||
| 180 | -``` | ||
| 181 | - | ||
| 182 | -### 需要修改 | ||
| 183 | -1. 编辑本文件修正内容 | ||
| 184 | -2. 重新提交审核 | ||
| 185 | - | ||
| 186 | -### 放弃本次解析 | ||
| 187 | -```bash | ||
| 188 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md | ||
| 189 | -``` | ||
| 190 | - | ||
| 191 | ---- | ||
| 192 | - | ||
| 193 | -## 审核状态 | ||
| 194 | - | ||
| 195 | -- [ ] 待审核 | ||
| 196 | -- [ ] 已通过 | ||
| 197 | -- [ ] 已拒绝 | ||
| 198 | - | ||
| 199 | -## 审核意见 | ||
| 200 | - | ||
| 201 | -```text | ||
| 202 | -``` |
| 1 | -# 产品配置审核 - 计划书模版2.docx | ||
| 2 | - | ||
| 3 | -**解析时间**: 2026/2/15 10:20:30 | ||
| 4 | -**原始文件**: 计划书模版2.docx | ||
| 5 | -**数据来源**: docs/to-parse/计划书模版2.docx | ||
| 6 | - | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -## 📋 产品基本信息 | ||
| 10 | - | ||
| 11 | -| 字段 | 提取值 | 需要确认 | | ||
| 12 | -|------|--------|---------| | ||
| 13 | -| 产品名称 | 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 | ✅ 请核对产品名称 | | ||
| 14 | -| 产品类型 | savings | ✅ 请确认产品类型 | | ||
| 15 | -| 币种 | USD | ✅ 请确认币种 | | ||
| 16 | -| form_sn | `savings-product-aaaa60f8` | ✅ 请确认 form_sn 唯一性 | | ||
| 17 | -| 缴费年期 | ["整付","3年","5年"] | ✅ 请确认缴费年期选项 | | ||
| 18 | -| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 | | ||
| 19 | -| 保险期间 | 终身 | ✅ 请确认保险期间 | | ||
| 20 | - | ||
| 21 | - | ||
| 22 | -### 💰 储蓄类产品特有字段 | ||
| 23 | - | ||
| 24 | -| 字段 | 提取值 | 需要确认 | | ||
| 25 | -|------|--------|---------| | ||
| 26 | -| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 | | ||
| 27 | -| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 | | ||
| 28 | - | ||
| 29 | - | ||
| 30 | ---- | ||
| 31 | - | ||
| 32 | -## 🤖 智能字段提取报告 | ||
| 33 | - | ||
| 34 | -### 匹配统计 | ||
| 35 | - | ||
| 36 | -- ✅ 成功匹配: 2 字段 | ||
| 37 | -- ⚠️ 使用默认值: 4 字段 | ||
| 38 | -- ❌ 未匹配(需人工补充): 0 字段 | ||
| 39 | - | ||
| 40 | -### ✅ 已成功匹配的字段 | ||
| 41 | - | ||
| 42 | -- product_name | ||
| 43 | -- payment_periods | ||
| 44 | - | ||
| 45 | - | ||
| 46 | -### ⚠️ 使用默认值的字段 | ||
| 47 | - | ||
| 48 | -- **product_type**: 未找到字段 "product_type",使用默认值: "savings" | ||
| 49 | -- **currency**: 未找到字段 "currency",使用默认值: "USD" | ||
| 50 | -- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75} | ||
| 51 | -- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身" | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - | ||
| 55 | - | ||
| 56 | ---- | ||
| 57 | - | ||
| 58 | -## 🧾 配置预览 | ||
| 59 | - | ||
| 60 | -```javascript | ||
| 61 | -{ | ||
| 62 | - "product_name": "宏摯家傳承保險計劃- 性別, 年齡, 出生年月日", | ||
| 63 | - "product_type": "savings", | ||
| 64 | - "currency": "USD", | ||
| 65 | - "form_sn": "savings-product-aaaa60f8", | ||
| 66 | - "payment_periods": [ | ||
| 67 | - "整付", | ||
| 68 | - "3年", | ||
| 69 | - "5年" | ||
| 70 | - ], | ||
| 71 | - "age_range": { | ||
| 72 | - "min": 0, | ||
| 73 | - "max": 75 | ||
| 74 | - }, | ||
| 75 | - "insurance_period": "终身", | ||
| 76 | - "is_savings": true, | ||
| 77 | - "withdrawal_modes": [ | ||
| 78 | - "年龄指定金额", | ||
| 79 | - "最高固定金额" | ||
| 80 | - ], | ||
| 81 | - "withdrawal_periods": [ | ||
| 82 | - "1年", | ||
| 83 | - "3年", | ||
| 84 | - "5年", | ||
| 85 | - "10年" | ||
| 86 | - ] | ||
| 87 | -} | ||
| 88 | -``` | ||
| 89 | - | ||
| 90 | ---- | ||
| 91 | - | ||
| 92 | -## 📝 表单字段 (form_schema) | ||
| 93 | - | ||
| 94 | -```javascript | ||
| 95 | -{ | ||
| 96 | - "base_fields": [], | ||
| 97 | - "withdrawal_fields": [], | ||
| 98 | - "reset_map": {} | ||
| 99 | -} | ||
| 100 | -``` | ||
| 101 | - | ||
| 102 | ---- | ||
| 103 | - | ||
| 104 | -## 🔄 提交字段映射 (submit_mapping) | ||
| 105 | - | ||
| 106 | -```javascript | ||
| 107 | -{} | ||
| 108 | -``` | ||
| 109 | - | ||
| 110 | ---- | ||
| 111 | - | ||
| 112 | -## 🧩 生成配置片段 | ||
| 113 | - | ||
| 114 | -```javascript | ||
| 115 | -/** | ||
| 116 | - * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 | ||
| 117 | - * @added 2026-02-15T02:20:30.997Z | ||
| 118 | - * @source docs/to-parse/计划书模版2.docx | ||
| 119 | - */ | ||
| 120 | - 'savings-product-aaaa60f8': { | ||
| 121 | - name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日', | ||
| 122 | - component: 'SavingsTemplate', | ||
| 123 | - category: 'savings', | ||
| 124 | - config: { | ||
| 125 | - currency: 'USD', | ||
| 126 | - payment_periods: ["整付","3年","5年"], | ||
| 127 | - age_range: { min: 0, max: 75 }, | ||
| 128 | - insurance_period: '终身', | ||
| 129 | - withdrawal_plan: { | ||
| 130 | - enabled: true, | ||
| 131 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 132 | - default_currency: 'USD', | ||
| 133 | - withdrawal_modes: ["年龄指定金额","最高固定金额"], | ||
| 134 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 135 | - }, | ||
| 136 | - form_schema: savingsFormSchema, | ||
| 137 | - submit_mapping: savingsSubmitMapping | ||
| 138 | - } | ||
| 139 | - } | ||
| 140 | -``` | ||
| 141 | - | ||
| 142 | ---- | ||
| 143 | - | ||
| 144 | -## ✅ 审核检查清单 | ||
| 145 | - | ||
| 146 | -### 基础信息 | ||
| 147 | -- [ ] 产品名称正确 | ||
| 148 | -- [ ] 产品类型正确(savings/critical-illness/life-insurance) | ||
| 149 | -- [ ] 币种正确(USD/CNY/HKD/EUR) | ||
| 150 | -- [ ] form_sn 唯一且符合命名规范 | ||
| 151 | - | ||
| 152 | -### 缴费与年龄 | ||
| 153 | -- [ ] 缴费年期选项完整且正确 | ||
| 154 | -- [ ] 年龄范围合理 | ||
| 155 | -- [ ] 保险期间正确 | ||
| 156 | - | ||
| 157 | -### 储蓄类特有(如适用) | ||
| 158 | -- [ ] 提取方式正确 | ||
| 159 | -- [ ] 提取期选项完整 | ||
| 160 | -- [ ] 表单字段定义完整 | ||
| 161 | -- [ ] 提交字段映射正确 | ||
| 162 | - | ||
| 163 | ---- | ||
| 164 | - | ||
| 165 | -## 📋 审核后操作 | ||
| 166 | - | ||
| 167 | -### 确认无误 | ||
| 168 | -```bash | ||
| 169 | -# 1. 移动到 approved 目录 | ||
| 170 | -mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md \ | ||
| 171 | - docs/parse-audit/approved/ | ||
| 172 | - | ||
| 173 | -# 2. 合并到正式配置 | ||
| 174 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 175 | - | ||
| 176 | -# 3. 删除待审核文件(可选) | ||
| 177 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md | ||
| 178 | -``` | ||
| 179 | - | ||
| 180 | -### 需要修改 | ||
| 181 | -1. 编辑本文件修正内容 | ||
| 182 | -2. 重新提交审核 | ||
| 183 | - | ||
| 184 | -### 放弃本次解析 | ||
| 185 | -```bash | ||
| 186 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md | ||
| 187 | -``` | ||
| 188 | - | ||
| 189 | ---- | ||
| 190 | - | ||
| 191 | -## 审核状态 | ||
| 192 | - | ||
| 193 | -- [ ] 待审核 | ||
| 194 | -- [ ] 已通过 | ||
| 195 | -- [ ] 已拒绝 | ||
| 196 | - | ||
| 197 | -## 审核意见 | ||
| 198 | - | ||
| 199 | -```text | ||
| 200 | -``` |
| 1 | -# 产品配置审核 - 计划书模版2.docx | ||
| 2 | - | ||
| 3 | -**解析时间**: 2026/2/15 10:20:30 | ||
| 4 | -**原始文件**: 计划书模版2.docx | ||
| 5 | -**数据来源**: docs/to-parse/计划书模版2.docx | ||
| 6 | - | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -## 📋 产品基本信息 | ||
| 10 | - | ||
| 11 | -| 字段 | 提取值 | 需要确认 | | ||
| 12 | -|------|--------|---------| | ||
| 13 | -| 产品名称 | 宏浚傳承保障計劃 | ✅ 请核对产品名称 | | ||
| 14 | -| 产品类型 | savings | ✅ 请确认产品类型 | | ||
| 15 | -| 币种 | USD | ✅ 请确认币种 | | ||
| 16 | -| form_sn | `savings-product-d1581522` | ✅ 请确认 form_sn 唯一性 | | ||
| 17 | -| 缴费年期 | ["整付","2年","5年"] | ✅ 请确认缴费年期选项 | | ||
| 18 | -| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 | | ||
| 19 | -| 保险期间 | 终身 | ✅ 请确认保险期间 | | ||
| 20 | - | ||
| 21 | - | ||
| 22 | -### 💰 储蓄类产品特有字段 | ||
| 23 | - | ||
| 24 | -| 字段 | 提取值 | 需要确认 | | ||
| 25 | -|------|--------|---------| | ||
| 26 | -| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 | | ||
| 27 | -| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 | | ||
| 28 | - | ||
| 29 | - | ||
| 30 | ---- | ||
| 31 | - | ||
| 32 | -## 🤖 智能字段提取报告 | ||
| 33 | - | ||
| 34 | -### 匹配统计 | ||
| 35 | - | ||
| 36 | -- ✅ 成功匹配: 2 字段 | ||
| 37 | -- ⚠️ 使用默认值: 4 字段 | ||
| 38 | -- ❌ 未匹配(需人工补充): 0 字段 | ||
| 39 | - | ||
| 40 | -### ✅ 已成功匹配的字段 | ||
| 41 | - | ||
| 42 | -- product_name | ||
| 43 | -- payment_periods | ||
| 44 | - | ||
| 45 | - | ||
| 46 | -### ⚠️ 使用默认值的字段 | ||
| 47 | - | ||
| 48 | -- **product_type**: 未找到字段 "product_type",使用默认值: "savings" | ||
| 49 | -- **currency**: 未找到字段 "currency",使用默认值: "USD" | ||
| 50 | -- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75} | ||
| 51 | -- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身" | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - | ||
| 55 | - | ||
| 56 | ---- | ||
| 57 | - | ||
| 58 | -## 🧾 配置预览 | ||
| 59 | - | ||
| 60 | -```javascript | ||
| 61 | -{ | ||
| 62 | - "product_name": "宏浚傳承保障計劃", | ||
| 63 | - "product_type": "savings", | ||
| 64 | - "currency": "USD", | ||
| 65 | - "form_sn": "savings-product-d1581522", | ||
| 66 | - "payment_periods": [ | ||
| 67 | - "整付", | ||
| 68 | - "2年", | ||
| 69 | - "5年" | ||
| 70 | - ], | ||
| 71 | - "age_range": { | ||
| 72 | - "min": 0, | ||
| 73 | - "max": 75 | ||
| 74 | - }, | ||
| 75 | - "insurance_period": "终身", | ||
| 76 | - "is_savings": true, | ||
| 77 | - "withdrawal_modes": [ | ||
| 78 | - "年龄指定金额", | ||
| 79 | - "最高固定金额" | ||
| 80 | - ], | ||
| 81 | - "withdrawal_periods": [ | ||
| 82 | - "1年", | ||
| 83 | - "3年", | ||
| 84 | - "5年", | ||
| 85 | - "10年" | ||
| 86 | - ] | ||
| 87 | -} | ||
| 88 | -``` | ||
| 89 | - | ||
| 90 | ---- | ||
| 91 | - | ||
| 92 | -## 📝 表单字段 (form_schema) | ||
| 93 | - | ||
| 94 | -```javascript | ||
| 95 | -{ | ||
| 96 | - "base_fields": [], | ||
| 97 | - "withdrawal_fields": [], | ||
| 98 | - "reset_map": {} | ||
| 99 | -} | ||
| 100 | -``` | ||
| 101 | - | ||
| 102 | ---- | ||
| 103 | - | ||
| 104 | -## 🔄 提交字段映射 (submit_mapping) | ||
| 105 | - | ||
| 106 | -```javascript | ||
| 107 | -{} | ||
| 108 | -``` | ||
| 109 | - | ||
| 110 | ---- | ||
| 111 | - | ||
| 112 | -## 🧩 生成配置片段 | ||
| 113 | - | ||
| 114 | -```javascript | ||
| 115 | -/** | ||
| 116 | - * 宏浚傳承保障計劃 | ||
| 117 | - * @added 2026-02-15T02:20:30.997Z | ||
| 118 | - * @source docs/to-parse/计划书模版2.docx | ||
| 119 | - */ | ||
| 120 | - 'savings-product-d1581522': { | ||
| 121 | - name: '宏浚傳承保障計劃', | ||
| 122 | - component: 'SavingsTemplate', | ||
| 123 | - category: 'savings', | ||
| 124 | - config: { | ||
| 125 | - currency: 'USD', | ||
| 126 | - payment_periods: ["整付","2年","5年"], | ||
| 127 | - age_range: { min: 0, max: 75 }, | ||
| 128 | - insurance_period: '终身', | ||
| 129 | - withdrawal_plan: { | ||
| 130 | - enabled: true, | ||
| 131 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 132 | - default_currency: 'USD', | ||
| 133 | - withdrawal_modes: ["年龄指定金额","最高固定金额"], | ||
| 134 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 135 | - }, | ||
| 136 | - form_schema: savingsFormSchema, | ||
| 137 | - submit_mapping: savingsSubmitMapping | ||
| 138 | - } | ||
| 139 | - } | ||
| 140 | -``` | ||
| 141 | - | ||
| 142 | ---- | ||
| 143 | - | ||
| 144 | -## ✅ 审核检查清单 | ||
| 145 | - | ||
| 146 | -### 基础信息 | ||
| 147 | -- [ ] 产品名称正确 | ||
| 148 | -- [ ] 产品类型正确(savings/critical-illness/life-insurance) | ||
| 149 | -- [ ] 币种正确(USD/CNY/HKD/EUR) | ||
| 150 | -- [ ] form_sn 唯一且符合命名规范 | ||
| 151 | - | ||
| 152 | -### 缴费与年龄 | ||
| 153 | -- [ ] 缴费年期选项完整且正确 | ||
| 154 | -- [ ] 年龄范围合理 | ||
| 155 | -- [ ] 保险期间正确 | ||
| 156 | - | ||
| 157 | -### 储蓄类特有(如适用) | ||
| 158 | -- [ ] 提取方式正确 | ||
| 159 | -- [ ] 提取期选项完整 | ||
| 160 | -- [ ] 表单字段定义完整 | ||
| 161 | -- [ ] 提交字段映射正确 | ||
| 162 | - | ||
| 163 | ---- | ||
| 164 | - | ||
| 165 | -## 📋 审核后操作 | ||
| 166 | - | ||
| 167 | -### 确认无误 | ||
| 168 | -```bash | ||
| 169 | -# 1. 移动到 approved 目录 | ||
| 170 | -mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md \ | ||
| 171 | - docs/parse-audit/approved/ | ||
| 172 | - | ||
| 173 | -# 2. 合并到正式配置 | ||
| 174 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 175 | - | ||
| 176 | -# 3. 删除待审核文件(可选) | ||
| 177 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md | ||
| 178 | -``` | ||
| 179 | - | ||
| 180 | -### 需要修改 | ||
| 181 | -1. 编辑本文件修正内容 | ||
| 182 | -2. 重新提交审核 | ||
| 183 | - | ||
| 184 | -### 放弃本次解析 | ||
| 185 | -```bash | ||
| 186 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md | ||
| 187 | -``` | ||
| 188 | - | ||
| 189 | ---- | ||
| 190 | - | ||
| 191 | -## 审核状态 | ||
| 192 | - | ||
| 193 | -- [ ] 待审核 | ||
| 194 | -- [ ] 已通过 | ||
| 195 | -- [ ] 已拒绝 | ||
| 196 | - | ||
| 197 | -## 审核意见 | ||
| 198 | - | ||
| 199 | -```text | ||
| 200 | -``` |
| 1 | -# 产品配置审核 - 计划书模版2.docx | ||
| 2 | - | ||
| 3 | -**解析时间**: 2026/2/15 10:20:30 | ||
| 4 | -**原始文件**: 计划书模版2.docx | ||
| 5 | -**数据来源**: docs/to-parse/计划书模版2.docx | ||
| 6 | - | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -## 📋 产品基本信息 | ||
| 10 | - | ||
| 11 | -| 字段 | 提取值 | 需要确认 | | ||
| 12 | -|------|--------|---------| | ||
| 13 | -| 产品名称 | 赤霞珠終身壽險計劃2基本人壽保障選項 | ✅ 请核对产品名称 | | ||
| 14 | -| 产品类型 | savings | ✅ 请确认产品类型 | | ||
| 15 | -| 币种 | USD | ✅ 请确认币种 | | ||
| 16 | -| form_sn | `savings-2-031c1237` | ✅ 请确认 form_sn 唯一性 | | ||
| 17 | -| 缴费年期 | ["5年","8年","12年","15年"] | ✅ 请确认缴费年期选项 | | ||
| 18 | -| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 | | ||
| 19 | -| 保险期间 | 终身 | ✅ 请确认保险期间 | | ||
| 20 | - | ||
| 21 | - | ||
| 22 | -### 💰 储蓄类产品特有字段 | ||
| 23 | - | ||
| 24 | -| 字段 | 提取值 | 需要确认 | | ||
| 25 | -|------|--------|---------| | ||
| 26 | -| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 | | ||
| 27 | -| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 | | ||
| 28 | - | ||
| 29 | - | ||
| 30 | ---- | ||
| 31 | - | ||
| 32 | -## 🤖 智能字段提取报告 | ||
| 33 | - | ||
| 34 | -### 匹配统计 | ||
| 35 | - | ||
| 36 | -- ✅ 成功匹配: 2 字段 | ||
| 37 | -- ⚠️ 使用默认值: 4 字段 | ||
| 38 | -- ❌ 未匹配(需人工补充): 0 字段 | ||
| 39 | - | ||
| 40 | -### ✅ 已成功匹配的字段 | ||
| 41 | - | ||
| 42 | -- product_name | ||
| 43 | -- payment_periods | ||
| 44 | - | ||
| 45 | - | ||
| 46 | -### ⚠️ 使用默认值的字段 | ||
| 47 | - | ||
| 48 | -- **product_type**: 未找到字段 "product_type",使用默认值: "savings" | ||
| 49 | -- **currency**: 未找到字段 "currency",使用默认值: "USD" | ||
| 50 | -- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75} | ||
| 51 | -- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身" | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - | ||
| 55 | - | ||
| 56 | ---- | ||
| 57 | - | ||
| 58 | -## 🧾 配置预览 | ||
| 59 | - | ||
| 60 | -```javascript | ||
| 61 | -{ | ||
| 62 | - "product_name": "赤霞珠終身壽險計劃2基本人壽保障選項", | ||
| 63 | - "product_type": "savings", | ||
| 64 | - "currency": "USD", | ||
| 65 | - "form_sn": "savings-2-031c1237", | ||
| 66 | - "payment_periods": [ | ||
| 67 | - "5年", | ||
| 68 | - "8年", | ||
| 69 | - "12年", | ||
| 70 | - "15年" | ||
| 71 | - ], | ||
| 72 | - "age_range": { | ||
| 73 | - "min": 0, | ||
| 74 | - "max": 75 | ||
| 75 | - }, | ||
| 76 | - "insurance_period": "终身", | ||
| 77 | - "is_savings": true, | ||
| 78 | - "withdrawal_modes": [ | ||
| 79 | - "年龄指定金额", | ||
| 80 | - "最高固定金额" | ||
| 81 | - ], | ||
| 82 | - "withdrawal_periods": [ | ||
| 83 | - "1年", | ||
| 84 | - "3年", | ||
| 85 | - "5年", | ||
| 86 | - "10年" | ||
| 87 | - ] | ||
| 88 | -} | ||
| 89 | -``` | ||
| 90 | - | ||
| 91 | ---- | ||
| 92 | - | ||
| 93 | -## 📝 表单字段 (form_schema) | ||
| 94 | - | ||
| 95 | -```javascript | ||
| 96 | -{ | ||
| 97 | - "base_fields": [], | ||
| 98 | - "withdrawal_fields": [], | ||
| 99 | - "reset_map": {} | ||
| 100 | -} | ||
| 101 | -``` | ||
| 102 | - | ||
| 103 | ---- | ||
| 104 | - | ||
| 105 | -## 🔄 提交字段映射 (submit_mapping) | ||
| 106 | - | ||
| 107 | -```javascript | ||
| 108 | -{} | ||
| 109 | -``` | ||
| 110 | - | ||
| 111 | ---- | ||
| 112 | - | ||
| 113 | -## 🧩 生成配置片段 | ||
| 114 | - | ||
| 115 | -```javascript | ||
| 116 | -/** | ||
| 117 | - * 赤霞珠終身壽險計劃2基本人壽保障選項 | ||
| 118 | - * @added 2026-02-15T02:20:30.997Z | ||
| 119 | - * @source docs/to-parse/计划书模版2.docx | ||
| 120 | - */ | ||
| 121 | - 'savings-2-031c1237': { | ||
| 122 | - name: '赤霞珠終身壽險計劃2基本人壽保障選項', | ||
| 123 | - component: 'SavingsTemplate', | ||
| 124 | - category: 'savings', | ||
| 125 | - config: { | ||
| 126 | - currency: 'USD', | ||
| 127 | - payment_periods: ["5年","8年","12年","15年"], | ||
| 128 | - age_range: { min: 0, max: 75 }, | ||
| 129 | - insurance_period: '终身', | ||
| 130 | - withdrawal_plan: { | ||
| 131 | - enabled: true, | ||
| 132 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 133 | - default_currency: 'USD', | ||
| 134 | - withdrawal_modes: ["年龄指定金额","最高固定金额"], | ||
| 135 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 136 | - }, | ||
| 137 | - form_schema: savingsFormSchema, | ||
| 138 | - submit_mapping: savingsSubmitMapping | ||
| 139 | - } | ||
| 140 | - } | ||
| 141 | -``` | ||
| 142 | - | ||
| 143 | ---- | ||
| 144 | - | ||
| 145 | -## ✅ 审核检查清单 | ||
| 146 | - | ||
| 147 | -### 基础信息 | ||
| 148 | -- [ ] 产品名称正确 | ||
| 149 | -- [ ] 产品类型正确(savings/critical-illness/life-insurance) | ||
| 150 | -- [ ] 币种正确(USD/CNY/HKD/EUR) | ||
| 151 | -- [ ] form_sn 唯一且符合命名规范 | ||
| 152 | - | ||
| 153 | -### 缴费与年龄 | ||
| 154 | -- [ ] 缴费年期选项完整且正确 | ||
| 155 | -- [ ] 年龄范围合理 | ||
| 156 | -- [ ] 保险期间正确 | ||
| 157 | - | ||
| 158 | -### 储蓄类特有(如适用) | ||
| 159 | -- [ ] 提取方式正确 | ||
| 160 | -- [ ] 提取期选项完整 | ||
| 161 | -- [ ] 表单字段定义完整 | ||
| 162 | -- [ ] 提交字段映射正确 | ||
| 163 | - | ||
| 164 | ---- | ||
| 165 | - | ||
| 166 | -## 📋 审核后操作 | ||
| 167 | - | ||
| 168 | -### 确认无误 | ||
| 169 | -```bash | ||
| 170 | -# 1. 移动到 approved 目录 | ||
| 171 | -mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md \ | ||
| 172 | - docs/parse-audit/approved/ | ||
| 173 | - | ||
| 174 | -# 2. 合并到正式配置 | ||
| 175 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 176 | - | ||
| 177 | -# 3. 删除待审核文件(可选) | ||
| 178 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md | ||
| 179 | -``` | ||
| 180 | - | ||
| 181 | -### 需要修改 | ||
| 182 | -1. 编辑本文件修正内容 | ||
| 183 | -2. 重新提交审核 | ||
| 184 | - | ||
| 185 | -### 放弃本次解析 | ||
| 186 | -```bash | ||
| 187 | -rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md | ||
| 188 | -``` | ||
| 189 | - | ||
| 190 | ---- | ||
| 191 | - | ||
| 192 | -## 审核状态 | ||
| 193 | - | ||
| 194 | -- [ ] 待审核 | ||
| 195 | -- [ ] 已通过 | ||
| 196 | -- [ ] 已拒绝 | ||
| 197 | - | ||
| 198 | -## 审核意见 | ||
| 199 | - | ||
| 200 | -```text | ||
| 201 | -``` |
docs/parsed-backup/backup-log.jsonl
deleted
100644 → 0
| 1 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771074633927.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-product-30b41aae"],"at":"2026-02-14T13:10:33.928Z"} | ||
| 2 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771077530778.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T13:58:50.779Z"} | ||
| 3 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771077569110.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T13:59:29.110Z"} | ||
| 4 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771077989896.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:06:29.896Z"} | ||
| 5 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078080604.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:08:00.605Z"} | ||
| 6 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078351660.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T14:12:31.660Z"} | ||
| 7 | -{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771080130974.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-55bcffc2"],"at":"2026-02-14T14:42:10.974Z"} |
docs/parsed-backup/parse-audit.jsonl
deleted
100644 → 0
| 1 | -{"at":"2026-02-14T13:10:33.928Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":3,"success_list":[{"form_sn":"savings-product-30b41aae","product_name":"测试计划书-智享未来","file":"测试计划书-智享未来.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-product-30b41aae"],"conflicts":[],"reason":null}} | ||
| 2 | -{"at":"2026-02-14T13:58:50.780Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-2-148b3acd"],"conflicts":[],"reason":null}} | ||
| 3 | -{"at":"2026-02-14T13:59:29.111Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-readme-a4296d1f","product_name":"README","file":"README.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-readme-a4296d1f"],"conflicts":[],"reason":null}} | ||
| 4 | -{"at":"2026-02-14T14:06:15.148Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-readme-a4296d1f","product_name":"README","file":"README.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-readme-a4296d1f"],"reason":"conflict"}} | ||
| 5 | -{"at":"2026-02-14T14:06:29.897Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-readme-a4296d1f","product_name":"README","file":"README.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-readme-a4296d1f"],"conflicts":[],"reason":null}} | ||
| 6 | -{"at":"2026-02-14T14:08:00.605Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-readme-a4296d1f","product_name":"README","file":"README.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-readme-a4296d1f"],"conflicts":[],"reason":null}} | ||
| 7 | -{"at":"2026-02-14T14:12:31.661Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":1,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-2-148b3acd"],"conflicts":[],"reason":null}} | ||
| 8 | -{"at":"2026-02-14T14:34:05.582Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-2-148b3acd"],"reason":"conflict"}} | ||
| 9 | -{"at":"2026-02-14T14:34:22.438Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-2-148b3acd"],"reason":"conflict"}} | ||
| 10 | -{"at":"2026-02-14T14:34:50.292Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":1153,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-2-148b3acd"],"reason":"conflict"}} | ||
| 11 | -{"at":"2026-02-14T14:35:12.489Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":6,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-2-148b3acd"],"reason":"conflict"}} | ||
| 12 | -{"at":"2026-02-14T14:35:32.726Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":2,"success_list":[{"form_sn":"savings-2-148b3acd","product_name":"测试计划书-智享未来2","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":["savings-2-148b3acd"],"reason":"conflict"}} | ||
| 13 | -{"at":"2026-02-14T14:42:10.975Z","mode":"single","options":{"dry_run":false},"summary":{"total":1,"success":1,"failed":0,"duration_ms":28,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["savings-2-55bcffc2"],"conflicts":[],"reason":null}} | ||
| 14 | -{"at":"2026-02-14T15:16:37.454Z","mode":"single","options":{"dry_run":true},"summary":{"total":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-product-54cf664a","product_name":"智享未来储蓄计划书","file":"测试计划书-智享未来2.md"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-product-54cf664a"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 智享未来储蓄计划书\n+ * @added 2026-02-14T15:16:37.415Z\n+ * @source docs/to-parse/测试计划书-智享未来2.md\n+ */\n+ 'savings-product-54cf664a': {\n+ name: '智享未来储蓄计划书',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 15 | -{"at":"2026-02-14T15:24:05.580Z","mode":"batch","options":{"dry_run":true},"summary":{"total":1,"success":1,"failed":0,"duration_ms":41,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-2-55bcffc2"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版2\n+ * @added 2026-02-14T15:24:05.564Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-2-55bcffc2': {\n+ name: '计划书模版2',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 16 | -{"at":"2026-02-14T15:29:02.807Z","mode":"batch","options":{"dry_run":true},"summary":{"total":2,"success":2,"failed":0,"duration_ms":50,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"},{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":2,"form_sn_list":["savings-2-55bcffc2","savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版2\n+ * @added 2026-02-14T15:29:02.781Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-2-55bcffc2': {\n+ name: '计划书模版2',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-14T15:29:02.804Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 17 | -{"at":"2026-02-14T15:30:22.307Z","mode":"batch","options":{"dry_run":true},"summary":{"total":2,"success":2,"failed":0,"duration_ms":49,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"},{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":2,"form_sn_list":["savings-2-55bcffc2","savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版2\n+ * @added 2026-02-14T15:30:22.283Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-2-55bcffc2': {\n+ name: '计划书模版2',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-14T15:30:22.304Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 18 | -{"at":"2026-02-14T15:51:38.101Z","mode":"batch","options":{"dry_run":true},"summary":{"total":2,"success":2,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"},{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":2,"form_sn_list":["savings-2-55bcffc2","savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版2\n+ * @added 2026-02-14T15:51:38.078Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-2-55bcffc2': {\n+ name: '计划书模版2',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-14T15:51:38.098Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 19 | -{"at":"2026-02-14T15:58:54.732Z","mode":"batch","options":{"dry_run":true},"summary":{"total":2,"success":2,"failed":0,"duration_ms":51,"success_list":[{"form_sn":"savings-2-55bcffc2","product_name":"计划书模版2","file":"计划书模版2.docx"},{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":2,"form_sn_list":["savings-2-55bcffc2","savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版2\n+ * @added 2026-02-14T15:58:54.707Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-2-55bcffc2': {\n+ name: '计划书模版2',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"指定提取金额\",\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-14T15:58:54.729Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 20 | -{"at":"2026-02-14T16:37:53.682Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":48,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":[],"reason":"insert_not_found"}} | ||
| 21 | -{"at":"2026-02-14T16:38:42.112Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":[],"reason":"insert_not_found"}} | ||
| 22 | -{"at":"2026-02-14T16:39:13.208Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":46,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":[],"reason":"insert_not_found"}} | ||
| 23 | -{"at":"2026-02-14T16:39:23.952Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":46,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":[],"reason":"insert_not_found"}} | ||
| 24 | -{"at":"2026-02-14T16:39:56.947Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":46,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":false,"dry_run":false,"updated_count":0,"form_sn_list":[],"conflicts":[],"reason":"insert_not_found"}} | ||
| 25 | -{"at":"2026-02-14T16:41:18.047Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":46,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-14T16:41:18.029Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-14T16:41:18.044Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-14T16:41:18.044Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} | ||
| 26 | -{"at":"2026-02-14T16:41:27.896Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[]},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-14T16:41:27.878Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-14T16:41:27.893Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-14T16:41:27.893Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} | ||
| 27 | -{"at":"2026-02-15T02:10:06.819Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":2,"total_products":5,"success":5,"failed":0,"duration_ms":73,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"},{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[],"total":5},"change_summary":{"ok":true,"dry_run":true,"updated_count":5,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237","savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:10:06.768Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:10:06.803Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T02:10:06.803Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} | ||
| 28 | -{"at":"2026-02-15T02:19:44.455Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-15T02:19:44.438Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"8年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 29 | -{"at":"2026-02-15T02:20:31.001Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.982Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -export const PLAN_TEMPLATES = { | ||
| 38 | - // 人寿保险产品 - WIOP3E | ||
| 39 | - 'life-insurance-wiop3e': { | ||
| 40 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 41 | - component: 'LifeInsuranceTemplate', | ||
| 42 | - config: { | ||
| 43 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 44 | - payment_periods: [ | ||
| 45 | - // 缴费年期选项 | ||
| 46 | - '整付(0-75 岁)', | ||
| 47 | - '5 年(0-70 岁)', | ||
| 48 | - '10 年(0-70 岁)' | ||
| 49 | - ], | ||
| 50 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 51 | - insurance_period: '终身' // 保险期间 | ||
| 52 | - } | ||
| 53 | - }, | ||
| 54 | - | ||
| 55 | - // 人寿保险产品 - WIOP3 | ||
| 56 | - 'life-insurance-wiop3': { | ||
| 57 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 58 | - component: 'LifeInsuranceTemplate', | ||
| 59 | - config: { | ||
| 60 | - currency: 'USD', | ||
| 61 | - payment_periods: [ | ||
| 62 | - '整付(0-75 岁)', | ||
| 63 | - '5 年(0-70 岁)', | ||
| 64 | - '10 年(0-70 岁)' | ||
| 65 | - ], | ||
| 66 | - age_range: { min: 0, max: 75 }, | ||
| 67 | - insurance_period: '终身' | ||
| 68 | - } | ||
| 69 | - }, | ||
| 70 | - | ||
| 71 | - // 重疾保险产品 - MPC | ||
| 72 | - 'critical-illness-mpc': { | ||
| 73 | - name: 'MPC 守护无间重疾', | ||
| 74 | - component: 'CriticalIllnessTemplate', | ||
| 75 | - config: { | ||
| 76 | - currency: 'USD', | ||
| 77 | - payment_periods: [ | ||
| 78 | - '10 年(15 日 - 65 岁)', | ||
| 79 | - '20 年(15 日 - 65 岁)', | ||
| 80 | - '25 年(15 日 - 60 岁)' | ||
| 81 | - ], | ||
| 82 | - age_range: { min: 0, max: 65 }, | ||
| 83 | - insurance_period: '终身' | ||
| 84 | - } | ||
| 85 | - }, | ||
| 86 | - | ||
| 87 | - // 重疾保险产品 - MBC PRO | ||
| 88 | - 'critical-illness-mbc-pro': { | ||
| 89 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 90 | - component: 'CriticalIllnessTemplate', | ||
| 91 | - config: { | ||
| 92 | - currency: 'USD', | ||
| 93 | - payment_periods: [ | ||
| 94 | - '10 年(15 日 - 65 岁)', | ||
| 95 | - '20 年(15 日 - 65 岁)', | ||
| 96 | - '25 年(15 日 - 60 岁)' | ||
| 97 | - ], | ||
| 98 | - age_range: { min: 0, max: 65 }, | ||
| 99 | - insurance_period: '终身' | ||
| 100 | - } | ||
| 101 | - }, | ||
| 102 | - | ||
| 103 | - // 重疾保险产品 - MBC2 | ||
| 104 | - 'critical-illness-mbc2': { | ||
| 105 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 106 | - component: 'CriticalIllnessTemplate', | ||
| 107 | - config: { | ||
| 108 | - currency: 'USD', | ||
| 109 | - payment_periods: [ | ||
| 110 | - '10 年(15 日 - 65 岁)', | ||
| 111 | - '20 年(15 日 - 65 岁)', | ||
| 112 | - '25 年(15 日 - 60 岁)' | ||
| 113 | - ], | ||
| 114 | - age_range: { min: 0, max: 65 }, | ||
| 115 | - insurance_period: '终身' | ||
| 116 | - } | ||
| 117 | - }, | ||
| 118 | - | ||
| 119 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 120 | - | ||
| 121 | - // GS - 宏挚传承保障计划 | ||
| 122 | - 'savings-gs': { | ||
| 123 | - name: '宏挚传承保障计划', | ||
| 124 | - component: 'SavingsTemplate', | ||
| 125 | - category: 'savings', // 储蓄型产品 | ||
| 126 | - config: { | ||
| 127 | - currency: 'USD', // 默认美元 | ||
| 128 | - payment_periods: [ | ||
| 129 | - '整付', | ||
| 130 | - '3 年', | ||
| 131 | - '5 年', | ||
| 132 | - '10 年', | ||
| 133 | - '15 年', | ||
| 134 | - ], | ||
| 135 | - age_range: { min: 0, max: 100 }, | ||
| 136 | - insurance_period: '终身', | ||
| 137 | - // 提取计划配置 | ||
| 138 | - withdrawal_plan: { | ||
| 139 | - enabled: true, | ||
| 140 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 141 | - default_currency: 'USD', // 统一为美元 | ||
| 142 | - withdrawal_modes: [ | ||
| 143 | - '年龄指定金额', // 方式1 | ||
| 144 | - '最高固定金额' // 方式2 | ||
| 145 | - ], | ||
| 146 | - withdrawal_periods: [ | ||
| 147 | - '1年', | ||
| 148 | - '2年', | ||
| 149 | - '3年', | ||
| 150 | - '5年', | ||
| 151 | - '10年', | ||
| 152 | - '15年', | ||
| 153 | - '20年', | ||
| 154 | - '终身' | ||
| 155 | - ] | ||
| 156 | - } | ||
| 157 | - } | ||
| 158 | - }, | ||
| 159 | - | ||
| 160 | - // GC - 宏挚家传保险计划 | ||
| 161 | - 'savings-gc': { | ||
| 162 | - name: '宏挚家传保险计划', | ||
| 163 | - component: 'SavingsTemplate', | ||
| 164 | - category: 'savings', | ||
| 165 | - config: { | ||
| 166 | - currency: 'USD', | ||
| 167 | - payment_periods: [ | ||
| 168 | - '整付', | ||
| 169 | - '3 年', | ||
| 170 | - '5 年', | ||
| 171 | - ], | ||
| 172 | - age_range: { min: 0, max: 100 }, | ||
| 173 | - insurance_period: '终身', | ||
| 174 | - withdrawal_plan: { | ||
| 175 | - enabled: true, | ||
| 176 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 177 | - default_currency: 'USD', // 统一为美元 | ||
| 178 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 179 | - withdrawal_periods: [ | ||
| 180 | - '1年', | ||
| 181 | - '2年', | ||
| 182 | - '3年', | ||
| 183 | - '5年', | ||
| 184 | - '10年', | ||
| 185 | - '15年', | ||
| 186 | - '20年', | ||
| 187 | - '终身' | ||
| 188 | - ] | ||
| 189 | - } | ||
| 190 | - } | ||
| 191 | - }, | ||
| 192 | - | ||
| 193 | - // FA - 宏浚传承保障计划 | ||
| 194 | - 'savings-fa': { | ||
| 195 | - name: '宏浚传承保障计划', | ||
| 196 | - component: 'SavingsTemplate', | ||
| 197 | - category: 'savings', | ||
| 198 | - config: { | ||
| 199 | - currency: 'USD', | ||
| 200 | - payment_periods: [ | ||
| 201 | - '整付', | ||
| 202 | - '2 年', | ||
| 203 | - '5 年', | ||
| 204 | - ], | ||
| 205 | - age_range: { min: 0, max: 100 }, | ||
| 206 | - insurance_period: '终身', | ||
| 207 | - withdrawal_plan: { | ||
| 208 | - enabled: true, | ||
| 209 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 210 | - default_currency: 'USD', // 统一为美元 | ||
| 211 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 212 | - withdrawal_periods: [ | ||
| 213 | - '1年', | ||
| 214 | - '2年', | ||
| 215 | - '3年', | ||
| 216 | - '5年', | ||
| 217 | - '10年', | ||
| 218 | - '15年', | ||
| 219 | - '20年', | ||
| 220 | - '终身' | ||
| 221 | - ] | ||
| 222 | - } | ||
| 223 | - } | ||
| 224 | - }, | ||
| 225 | - | ||
| 226 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 227 | - 'savings-lv2': { | ||
| 228 | - name: '赤霞珠终身寿险计划2', | ||
| 229 | - component: 'SavingsTemplate', | ||
| 230 | - category: 'savings', | ||
| 231 | - config: { | ||
| 232 | - currency: 'USD', | ||
| 233 | - payment_periods: [ | ||
| 234 | - '5 年', | ||
| 235 | - '8 年', | ||
| 236 | - '12 年', | ||
| 237 | - '15 年', | ||
| 238 | - ], | ||
| 239 | - age_range: { min: 0, max: 100 }, | ||
| 240 | - insurance_period: '终身', | ||
| 241 | - withdrawal_plan: { | ||
| 242 | - enabled: true, | ||
| 243 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 244 | - default_currency: 'USD', // 统一为美元 | ||
| 245 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 246 | - withdrawal_periods: [ | ||
| 247 | - '1年', | ||
| 248 | - '2年', | ||
| 249 | - '3年', | ||
| 250 | - '5年', | ||
| 251 | - '10年', | ||
| 252 | - '15年', | ||
| 253 | - '20年', | ||
| 254 | - '终身' | ||
| 255 | - ] | ||
| 256 | - } | ||
| 257 | - } | ||
| 258 | - } | ||
| 259 | -} | ||
| 260 | - | ||
| 261 | -/** | ||
| 262 | - * 全局功能开关 | ||
| 263 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 264 | - * | ||
| 265 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 266 | - */ | ||
| 267 | -export const FEATURE_FLAGS = { | ||
| 268 | - /** | ||
| 269 | - * 多币种切换功能 | ||
| 270 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 271 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 272 | - * @type {boolean} | ||
| 273 | - */ | ||
| 274 | - MULTI_CURRENCY_ENABLED: false | ||
| 275 | -} | ||
| 276 | - | ||
| 277 | -/** | ||
| 278 | - * 币种符号映射 | ||
| 279 | - * @description 币种代码到符号的映射关系 | ||
| 280 | - */ | ||
| 281 | -export const CURRENCY_SYMBOLS = { | ||
| 282 | - CNY: '¥', // 人民币 | ||
| 283 | - USD: '$', // 美元 | ||
| 284 | - HKD: 'HK$', // 港币 | ||
| 285 | - EUR: '€' // 欧元 | ||
| 286 | -} | ||
| 287 | - | ||
| 288 | -/** | ||
| 289 | - * 币种完整信息映射 | ||
| 290 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 291 | - */ | ||
| 292 | -export const CURRENCY_MAP = { | ||
| 293 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 294 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 295 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 296 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 297 | -} | ||
| 298 | - | ||
| 299 | -/** | ||
| 300 | - * 根据 form_sn 获取模版配置 | ||
| 301 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 302 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 303 | - * | ||
| 304 | - * @example | ||
| 305 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 306 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 307 | - */ | ||
| 308 | -export function getTemplateConfig(formSn) { | ||
| 309 | - if (!formSn) { | ||
| 310 | - console.warn('[plan-templates] form_sn 为空') | ||
| 311 | - return null | ||
| 312 | - } | ||
| 313 | - | ||
| 314 | - const config = PLAN_TEMPLATES[formSn] | ||
| 315 | - if (!config) { | ||
| 316 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 317 | - return null | ||
| 318 | - } | ||
| 319 | - | ||
| 320 | - return config | ||
| 321 | -} | ||
| 322 | - | ||
| 323 | -/** | ||
| 324 | - * 获取币种符号 | ||
| 325 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 326 | - * @returns {string} 币种符号 | ||
| 327 | - * | ||
| 328 | - * @example | ||
| 329 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 330 | - */ | ||
| 331 | -export function getCurrencySymbol(currencyCode) { | ||
| 332 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 333 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -export const PLAN_TEMPLATES = { | ||
| 38 | - // 人寿保险产品 - WIOP3E | ||
| 39 | - 'life-insurance-wiop3e': { | ||
| 40 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 41 | - component: 'LifeInsuranceTemplate', | ||
| 42 | - config: { | ||
| 43 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 44 | - payment_periods: [ | ||
| 45 | - // 缴费年期选项 | ||
| 46 | - '整付(0-75 岁)', | ||
| 47 | - '5 年(0-70 岁)', | ||
| 48 | - '10 年(0-70 岁)' | ||
| 49 | - ], | ||
| 50 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 51 | - insurance_period: '终身' // 保险期间 | ||
| 52 | - } | ||
| 53 | - }, | ||
| 54 | - | ||
| 55 | - // 人寿保险产品 - WIOP3 | ||
| 56 | - 'life-insurance-wiop3': { | ||
| 57 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 58 | - component: 'LifeInsuranceTemplate', | ||
| 59 | - config: { | ||
| 60 | - currency: 'USD', | ||
| 61 | - payment_periods: [ | ||
| 62 | - '整付(0-75 岁)', | ||
| 63 | - '5 年(0-70 岁)', | ||
| 64 | - '10 年(0-70 岁)' | ||
| 65 | - ], | ||
| 66 | - age_range: { min: 0, max: 75 }, | ||
| 67 | - insurance_period: '终身' | ||
| 68 | - } | ||
| 69 | - }, | ||
| 70 | - | ||
| 71 | - // 重疾保险产品 - MPC | ||
| 72 | - 'critical-illness-mpc': { | ||
| 73 | - name: 'MPC 守护无间重疾', | ||
| 74 | - component: 'CriticalIllnessTemplate', | ||
| 75 | - config: { | ||
| 76 | - currency: 'USD', | ||
| 77 | - payment_periods: [ | ||
| 78 | - '10 年(15 日 - 65 岁)', | ||
| 79 | - '20 年(15 日 - 65 岁)', | ||
| 80 | - '25 年(15 日 - 60 岁)' | ||
| 81 | - ], | ||
| 82 | - age_range: { min: 0, max: 65 }, | ||
| 83 | - insurance_period: '终身' | ||
| 84 | - } | ||
| 85 | - }, | ||
| 86 | - | ||
| 87 | - // 重疾保险产品 - MBC PRO | ||
| 88 | - 'critical-illness-mbc-pro': { | ||
| 89 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 90 | - component: 'CriticalIllnessTemplate', | ||
| 91 | - config: { | ||
| 92 | - currency: 'USD', | ||
| 93 | - payment_periods: [ | ||
| 94 | - '10 年(15 日 - 65 岁)', | ||
| 95 | - '20 年(15 日 - 65 岁)', | ||
| 96 | - '25 年(15 日 - 60 岁)' | ||
| 97 | - ], | ||
| 98 | - age_range: { min: 0, max: 65 }, | ||
| 99 | - insurance_period: '终身' | ||
| 100 | - } | ||
| 101 | - }, | ||
| 102 | - | ||
| 103 | - // 重疾保险产品 - MBC2 | ||
| 104 | - 'critical-illness-mbc2': { | ||
| 105 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 106 | - component: 'CriticalIllnessTemplate', | ||
| 107 | - config: { | ||
| 108 | - currency: 'USD', | ||
| 109 | - payment_periods: [ | ||
| 110 | - '10 年(15 日 - 65 岁)', | ||
| 111 | - '20 年(15 日 - 65 岁)', | ||
| 112 | - '25 年(15 日 - 60 岁)' | ||
| 113 | - ], | ||
| 114 | - age_range: { min: 0, max: 65 }, | ||
| 115 | - insurance_period: '终身' | ||
| 116 | - } | ||
| 117 | - }, | ||
| 118 | - | ||
| 119 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 120 | - | ||
| 121 | - // GS - 宏挚传承保障计划 | ||
| 122 | - 'savings-gs': { | ||
| 123 | - name: '宏挚传承保障计划', | ||
| 124 | - component: 'SavingsTemplate', | ||
| 125 | - category: 'savings', // 储蓄型产品 | ||
| 126 | - config: { | ||
| 127 | - currency: 'USD', // 默认美元 | ||
| 128 | - payment_periods: [ | ||
| 129 | - '整付', | ||
| 130 | - '3 年', | ||
| 131 | - '5 年', | ||
| 132 | - '10 年', | ||
| 133 | - '15 年', | ||
| 134 | - ], | ||
| 135 | - age_range: { min: 0, max: 100 }, | ||
| 136 | - insurance_period: '终身', | ||
| 137 | - // 提取计划配置 | ||
| 138 | - withdrawal_plan: { | ||
| 139 | - enabled: true, | ||
| 140 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 141 | - default_currency: 'USD', // 统一为美元 | ||
| 142 | - withdrawal_modes: [ | ||
| 143 | - '年龄指定金额', // 方式1 | ||
| 144 | - '最高固定金额' // 方式2 | ||
| 145 | - ], | ||
| 146 | - withdrawal_periods: [ | ||
| 147 | - '1年', | ||
| 148 | - '2年', | ||
| 149 | - '3年', | ||
| 150 | - '5年', | ||
| 151 | - '10年', | ||
| 152 | - '15年', | ||
| 153 | - '20年', | ||
| 154 | - '终身' | ||
| 155 | - ] | ||
| 156 | - } | ||
| 157 | - } | ||
| 158 | - }, | ||
| 159 | - | ||
| 160 | - // GC - 宏挚家传保险计划 | ||
| 161 | - 'savings-gc': { | ||
| 162 | - name: '宏挚家传保险计划', | ||
| 163 | - component: 'SavingsTemplate', | ||
| 164 | - category: 'savings', | ||
| 165 | - config: { | ||
| 166 | - currency: 'USD', | ||
| 167 | - payment_periods: [ | ||
| 168 | - '整付', | ||
| 169 | - '3 年', | ||
| 170 | - '5 年', | ||
| 171 | - ], | ||
| 172 | - age_range: { min: 0, max: 100 }, | ||
| 173 | - insurance_period: '终身', | ||
| 174 | - withdrawal_plan: { | ||
| 175 | - enabled: true, | ||
| 176 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 177 | - default_currency: 'USD', // 统一为美元 | ||
| 178 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 179 | - withdrawal_periods: [ | ||
| 180 | - '1年', | ||
| 181 | - '2年', | ||
| 182 | - '3年', | ||
| 183 | - '5年', | ||
| 184 | - '10年', | ||
| 185 | - '15年', | ||
| 186 | - '20年', | ||
| 187 | - '终身' | ||
| 188 | - ] | ||
| 189 | - } | ||
| 190 | - } | ||
| 191 | - }, | ||
| 192 | - | ||
| 193 | - // FA - 宏浚传承保障计划 | ||
| 194 | - 'savings-fa': { | ||
| 195 | - name: '宏浚传承保障计划', | ||
| 196 | - component: 'SavingsTemplate', | ||
| 197 | - category: 'savings', | ||
| 198 | - config: { | ||
| 199 | - currency: 'USD', | ||
| 200 | - payment_periods: [ | ||
| 201 | - '整付', | ||
| 202 | - '2 年', | ||
| 203 | - '5 年', | ||
| 204 | - ], | ||
| 205 | - age_range: { min: 0, max: 100 }, | ||
| 206 | - insurance_period: '终身', | ||
| 207 | - withdrawal_plan: { | ||
| 208 | - enabled: true, | ||
| 209 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 210 | - default_currency: 'USD', // 统一为美元 | ||
| 211 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 212 | - withdrawal_periods: [ | ||
| 213 | - '1年', | ||
| 214 | - '2年', | ||
| 215 | - '3年', | ||
| 216 | - '5年', | ||
| 217 | - '10年', | ||
| 218 | - '15年', | ||
| 219 | - '20年', | ||
| 220 | - '终身' | ||
| 221 | - ] | ||
| 222 | - } | ||
| 223 | - } | ||
| 224 | - }, | ||
| 225 | - | ||
| 226 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 227 | - 'savings-lv2': { | ||
| 228 | - name: '赤霞珠终身寿险计划2', | ||
| 229 | - component: 'SavingsTemplate', | ||
| 230 | - category: 'savings', | ||
| 231 | - config: { | ||
| 232 | - currency: 'USD', | ||
| 233 | - payment_periods: [ | ||
| 234 | - '5 年', | ||
| 235 | - '8 年', | ||
| 236 | - '12 年', | ||
| 237 | - '15 年', | ||
| 238 | - ], | ||
| 239 | - age_range: { min: 0, max: 100 }, | ||
| 240 | - insurance_period: '终身', | ||
| 241 | - withdrawal_plan: { | ||
| 242 | - enabled: true, | ||
| 243 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 244 | - default_currency: 'USD', // 统一为美元 | ||
| 245 | - withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 246 | - withdrawal_periods: [ | ||
| 247 | - '1年', | ||
| 248 | - '2年', | ||
| 249 | - '3年', | ||
| 250 | - '5年', | ||
| 251 | - '10年', | ||
| 252 | - '15年', | ||
| 253 | - '20年', | ||
| 254 | - '终身' | ||
| 255 | - ] | ||
| 256 | - } | ||
| 257 | - } | ||
| 258 | - } | ||
| 259 | -} | ||
| 260 | - | ||
| 261 | -/** | ||
| 262 | - * 全局功能开关 | ||
| 263 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 264 | - * | ||
| 265 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 266 | - */ | ||
| 267 | -export const FEATURE_FLAGS = { | ||
| 268 | - /** | ||
| 269 | - * 多币种切换功能 | ||
| 270 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 271 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 272 | - * @type {boolean} | ||
| 273 | - */ | ||
| 274 | - MULTI_CURRENCY_ENABLED: false | ||
| 275 | -} | ||
| 276 | - | ||
| 277 | -/** | ||
| 278 | - * 币种符号映射 | ||
| 279 | - * @description 币种代码到符号的映射关系 | ||
| 280 | - */ | ||
| 281 | -export const CURRENCY_SYMBOLS = { | ||
| 282 | - CNY: '¥', // 人民币 | ||
| 283 | - USD: '$', // 美元 | ||
| 284 | - HKD: 'HK$', // 港币 | ||
| 285 | - EUR: '€' // 欧元 | ||
| 286 | -} | ||
| 287 | - | ||
| 288 | -/** | ||
| 289 | - * 币种完整信息映射 | ||
| 290 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 291 | - */ | ||
| 292 | -export const CURRENCY_MAP = { | ||
| 293 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 294 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 295 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 296 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 297 | -} | ||
| 298 | - | ||
| 299 | -/** | ||
| 300 | - * 根据 form_sn 获取模版配置 | ||
| 301 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 302 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 303 | - * | ||
| 304 | - * @example | ||
| 305 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 306 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 307 | - */ | ||
| 308 | -export function getTemplateConfig(formSn) { | ||
| 309 | - if (!formSn) { | ||
| 310 | - console.warn('[plan-templates] form_sn 为空') | ||
| 311 | - return null | ||
| 312 | - } | ||
| 313 | - | ||
| 314 | - const config = PLAN_TEMPLATES[formSn] | ||
| 315 | - if (!config) { | ||
| 316 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 317 | - return null | ||
| 318 | - } | ||
| 319 | - | ||
| 320 | - return config | ||
| 321 | -} | ||
| 322 | - | ||
| 323 | -/** | ||
| 324 | - * 获取币种符号 | ||
| 325 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 326 | - * @returns {string} 币种符号 | ||
| 327 | - * | ||
| 328 | - * @example | ||
| 329 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 330 | - */ | ||
| 331 | -export function getCurrencySymbol(currencyCode) { | ||
| 332 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 333 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - } | ||
| 343 | -} | ||
| 344 | - | ||
| 345 | -/** | ||
| 346 | - * 全局功能开关 | ||
| 347 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 348 | - * | ||
| 349 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 350 | - */ | ||
| 351 | -export const FEATURE_FLAGS = { | ||
| 352 | - /** | ||
| 353 | - * 多币种切换功能 | ||
| 354 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 355 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 356 | - * @type {boolean} | ||
| 357 | - */ | ||
| 358 | - MULTI_CURRENCY_ENABLED: false | ||
| 359 | -} | ||
| 360 | - | ||
| 361 | -/** | ||
| 362 | - * 币种符号映射 | ||
| 363 | - * @description 币种代码到符号的映射关系 | ||
| 364 | - */ | ||
| 365 | -export const CURRENCY_SYMBOLS = { | ||
| 366 | - CNY: '¥', // 人民币 | ||
| 367 | - USD: '$', // 美元 | ||
| 368 | - HKD: 'HK$', // 港币 | ||
| 369 | - EUR: '€' // 欧元 | ||
| 370 | -} | ||
| 371 | - | ||
| 372 | -/** | ||
| 373 | - * 币种完整信息映射 | ||
| 374 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 375 | - */ | ||
| 376 | -export const CURRENCY_MAP = { | ||
| 377 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 378 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 379 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 380 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 381 | -} | ||
| 382 | - | ||
| 383 | -/** | ||
| 384 | - * 根据 form_sn 获取模版配置 | ||
| 385 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 386 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 387 | - * | ||
| 388 | - * @example | ||
| 389 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 390 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 391 | - */ | ||
| 392 | -export function getTemplateConfig(formSn) { | ||
| 393 | - if (!formSn) { | ||
| 394 | - console.warn('[plan-templates] form_sn 为空') | ||
| 395 | - return null | ||
| 396 | - } | ||
| 397 | - | ||
| 398 | - const config = PLAN_TEMPLATES[formSn] | ||
| 399 | - if (!config) { | ||
| 400 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 401 | - return null | ||
| 402 | - } | ||
| 403 | - | ||
| 404 | - return config | ||
| 405 | -} | ||
| 406 | - | ||
| 407 | -/** | ||
| 408 | - * 获取币种符号 | ||
| 409 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 410 | - * @returns {string} 币种符号 | ||
| 411 | - * | ||
| 412 | - * @example | ||
| 413 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 414 | - */ | ||
| 415 | -export function getCurrencySymbol(currencyCode) { | ||
| 416 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 417 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | - /** | ||
| 345 | - * 测试计划书-智享未来 | ||
| 346 | - * @added 2026-02-14T13:10:33.924Z | ||
| 347 | - * @source docs/to-parse/测试计划书-智享未来.md | ||
| 348 | - */ | ||
| 349 | - 'savings-product-30b41aae': { | ||
| 350 | - name: '测试计划书-智享未来', | ||
| 351 | - component: 'SavingsTemplate', | ||
| 352 | - category: 'savings', | ||
| 353 | - config: { | ||
| 354 | - currency: 'USD', | ||
| 355 | - payment_periods: ["整付", "3年", "5年"], | ||
| 356 | - age_range: { min: 0, max: 75 }, | ||
| 357 | - insurance_period: '终身', | ||
| 358 | - withdrawal_plan: { | ||
| 359 | - enabled: true, | ||
| 360 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 361 | - default_currency: 'USD', | ||
| 362 | - withdrawal_modes: ["年龄指定金额", "最高固定金额"], | ||
| 363 | - withdrawal_periods: ["1年", "3年", "5年", "10年"] | ||
| 364 | - }, | ||
| 365 | - form_schema: savingsFormSchema, | ||
| 366 | - submit_mapping: savingsSubmitMapping | ||
| 367 | - } | ||
| 368 | - } | ||
| 369 | -} | ||
| 370 | - | ||
| 371 | -/** | ||
| 372 | - * 全局功能开关 | ||
| 373 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 374 | - * | ||
| 375 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 376 | - */ | ||
| 377 | -export const FEATURE_FLAGS = { | ||
| 378 | - /** | ||
| 379 | - * 多币种切换功能 | ||
| 380 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 381 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 382 | - * @type {boolean} | ||
| 383 | - */ | ||
| 384 | - MULTI_CURRENCY_ENABLED: false | ||
| 385 | -} | ||
| 386 | - | ||
| 387 | -/** | ||
| 388 | - * 币种符号映射 | ||
| 389 | - * @description 币种代码到符号的映射关系 | ||
| 390 | - */ | ||
| 391 | -export const CURRENCY_SYMBOLS = { | ||
| 392 | - CNY: '¥', // 人民币 | ||
| 393 | - USD: '$', // 美元 | ||
| 394 | - HKD: 'HK$', // 港币 | ||
| 395 | - EUR: '€' // 欧元 | ||
| 396 | -} | ||
| 397 | - | ||
| 398 | -/** | ||
| 399 | - * 币种完整信息映射 | ||
| 400 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 401 | - */ | ||
| 402 | -export const CURRENCY_MAP = { | ||
| 403 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 404 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 405 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 406 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 407 | -} | ||
| 408 | - | ||
| 409 | -/** | ||
| 410 | - * 根据 form_sn 获取模版配置 | ||
| 411 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 412 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 413 | - * | ||
| 414 | - * @example | ||
| 415 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 416 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 417 | - */ | ||
| 418 | -export function getTemplateConfig(formSn) { | ||
| 419 | - if (!formSn) { | ||
| 420 | - console.warn('[plan-templates] form_sn 为空') | ||
| 421 | - return null | ||
| 422 | - } | ||
| 423 | - | ||
| 424 | - const config = PLAN_TEMPLATES[formSn] | ||
| 425 | - if (!config) { | ||
| 426 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 427 | - return null | ||
| 428 | - } | ||
| 429 | - | ||
| 430 | - return config | ||
| 431 | -} | ||
| 432 | - | ||
| 433 | -/** | ||
| 434 | - * 获取币种符号 | ||
| 435 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 436 | - * @returns {string} 币种符号 | ||
| 437 | - * | ||
| 438 | - * @example | ||
| 439 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 440 | - */ | ||
| 441 | -export function getCurrencySymbol(currencyCode) { | ||
| 442 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 443 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | - /** | ||
| 345 | - * 测试计划书-智享未来 | ||
| 346 | - * @added 2026-02-14T13:10:33.924Z | ||
| 347 | - * @source docs/to-parse/测试计划书-智享未来.md | ||
| 348 | - */ | ||
| 349 | - 'savings-product-30b41aae': { | ||
| 350 | - name: '测试计划书-智享未来', | ||
| 351 | - component: 'SavingsTemplate', | ||
| 352 | - category: 'savings', | ||
| 353 | - config: { | ||
| 354 | - currency: 'USD', | ||
| 355 | - payment_periods: ["整付", "3年", "5年"], | ||
| 356 | - age_range: { min: 0, max: 75 }, | ||
| 357 | - insurance_period: '终身', | ||
| 358 | - withdrawal_plan: { | ||
| 359 | - enabled: true, | ||
| 360 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 361 | - default_currency: 'USD', | ||
| 362 | - withdrawal_modes: ["年龄指定金额", "最高固定金额"], | ||
| 363 | - withdrawal_periods: ["1年", "3年", "5年", "10年"] | ||
| 364 | - }, | ||
| 365 | - form_schema: savingsFormSchema, | ||
| 366 | - submit_mapping: savingsSubmitMapping | ||
| 367 | - } | ||
| 368 | - }, | ||
| 369 | - | ||
| 370 | - /** | ||
| 371 | - * 测试计划书-智享未来2 | ||
| 372 | - * @added 2026-02-14T13:58:50.776Z | ||
| 373 | - * @source docs/to-parse/测试计划书-智享未来2.md | ||
| 374 | - */ | ||
| 375 | - 'savings-2-148b3acd': { | ||
| 376 | - name: '测试计划书-智享未来2', | ||
| 377 | - component: 'SavingsTemplate', | ||
| 378 | - category: 'savings', | ||
| 379 | - config: { | ||
| 380 | - currency: 'USD', | ||
| 381 | - payment_periods: ["整付","3年","5年"], | ||
| 382 | - age_range: { min: 0, max: 75 }, | ||
| 383 | - insurance_period: '终身', | ||
| 384 | - withdrawal_plan: { | ||
| 385 | - enabled: true, | ||
| 386 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 387 | - default_currency: 'USD', | ||
| 388 | - withdrawal_modes: ["年龄指定金额","最高固定金额"], | ||
| 389 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 390 | - }, | ||
| 391 | - form_schema: { | ||
| 392 | - "base_fields": [], | ||
| 393 | - "withdrawal_fields": [], | ||
| 394 | - "reset_map": {} | ||
| 395 | - }, | ||
| 396 | - submit_mapping: savingsSubmitMapping | ||
| 397 | - } | ||
| 398 | - }} | ||
| 399 | - | ||
| 400 | -/** | ||
| 401 | - * 全局功能开关 | ||
| 402 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 403 | - * | ||
| 404 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 405 | - */ | ||
| 406 | -export const FEATURE_FLAGS = { | ||
| 407 | - /** | ||
| 408 | - * 多币种切换功能 | ||
| 409 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 410 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 411 | - * @type {boolean} | ||
| 412 | - */ | ||
| 413 | - MULTI_CURRENCY_ENABLED: false | ||
| 414 | -} | ||
| 415 | - | ||
| 416 | -/** | ||
| 417 | - * 币种符号映射 | ||
| 418 | - * @description 币种代码到符号的映射关系 | ||
| 419 | - */ | ||
| 420 | -export const CURRENCY_SYMBOLS = { | ||
| 421 | - CNY: '¥', // 人民币 | ||
| 422 | - USD: '$', // 美元 | ||
| 423 | - HKD: 'HK$', // 港币 | ||
| 424 | - EUR: '€' // 欧元 | ||
| 425 | -} | ||
| 426 | - | ||
| 427 | -/** | ||
| 428 | - * 币种完整信息映射 | ||
| 429 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 430 | - */ | ||
| 431 | -export const CURRENCY_MAP = { | ||
| 432 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 433 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 434 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 435 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 436 | -} | ||
| 437 | - | ||
| 438 | -/** | ||
| 439 | - * 根据 form_sn 获取模版配置 | ||
| 440 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 441 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 442 | - * | ||
| 443 | - * @example | ||
| 444 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 445 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 446 | - */ | ||
| 447 | -export function getTemplateConfig(formSn) { | ||
| 448 | - if (!formSn) { | ||
| 449 | - console.warn('[plan-templates] form_sn 为空') | ||
| 450 | - return null | ||
| 451 | - } | ||
| 452 | - | ||
| 453 | - const config = PLAN_TEMPLATES[formSn] | ||
| 454 | - if (!config) { | ||
| 455 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 456 | - return null | ||
| 457 | - } | ||
| 458 | - | ||
| 459 | - return config | ||
| 460 | -} | ||
| 461 | - | ||
| 462 | -/** | ||
| 463 | - * 获取币种符号 | ||
| 464 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 465 | - * @returns {string} 币种符号 | ||
| 466 | - * | ||
| 467 | - * @example | ||
| 468 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 469 | - */ | ||
| 470 | -export function getCurrencySymbol(currencyCode) { | ||
| 471 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 472 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | - /** | ||
| 345 | - * 测试计划书-智享未来 | ||
| 346 | - * @added 2026-02-14T13:10:33.924Z | ||
| 347 | - * @source docs/to-parse/测试计划书-智享未来.md | ||
| 348 | - */ | ||
| 349 | - 'savings-product-30b41aae': { | ||
| 350 | - name: '测试计划书-智享未来', | ||
| 351 | - component: 'SavingsTemplate', | ||
| 352 | - category: 'savings', | ||
| 353 | - config: { | ||
| 354 | - currency: 'USD', | ||
| 355 | - payment_periods: ["整付", "3年", "5年"], | ||
| 356 | - age_range: { min: 0, max: 75 }, | ||
| 357 | - insurance_period: '终身', | ||
| 358 | - withdrawal_plan: { | ||
| 359 | - enabled: true, | ||
| 360 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 361 | - default_currency: 'USD', | ||
| 362 | - withdrawal_modes: ["年龄指定金额", "最高固定金额"], | ||
| 363 | - withdrawal_periods: ["1年", "3年", "5年", "10年"] | ||
| 364 | - }, | ||
| 365 | - form_schema: savingsFormSchema, | ||
| 366 | - submit_mapping: savingsSubmitMapping | ||
| 367 | - } | ||
| 368 | - }, | ||
| 369 | - | ||
| 370 | -} | ||
| 371 | - | ||
| 372 | -/** | ||
| 373 | - * 全局功能开关 | ||
| 374 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 375 | - * | ||
| 376 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 377 | - */ | ||
| 378 | -export const FEATURE_FLAGS = { | ||
| 379 | - /** | ||
| 380 | - * 多币种切换功能 | ||
| 381 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 382 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 383 | - * @type {boolean} | ||
| 384 | - */ | ||
| 385 | - MULTI_CURRENCY_ENABLED: false | ||
| 386 | -} | ||
| 387 | - | ||
| 388 | -/** | ||
| 389 | - * 币种符号映射 | ||
| 390 | - * @description 币种代码到符号的映射关系 | ||
| 391 | - */ | ||
| 392 | -export const CURRENCY_SYMBOLS = { | ||
| 393 | - CNY: '¥', // 人民币 | ||
| 394 | - USD: '$', // 美元 | ||
| 395 | - HKD: 'HK$', // 港币 | ||
| 396 | - EUR: '€' // 欧元 | ||
| 397 | -} | ||
| 398 | - | ||
| 399 | -/** | ||
| 400 | - * 币种完整信息映射 | ||
| 401 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 402 | - */ | ||
| 403 | -export const CURRENCY_MAP = { | ||
| 404 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 405 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 406 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 407 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 408 | -} | ||
| 409 | - | ||
| 410 | -/** | ||
| 411 | - * 根据 form_sn 获取模版配置 | ||
| 412 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 413 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 414 | - * | ||
| 415 | - * @example | ||
| 416 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 417 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 418 | - */ | ||
| 419 | -export function getTemplateConfig(formSn) { | ||
| 420 | - if (!formSn) { | ||
| 421 | - console.warn('[plan-templates] form_sn 为空') | ||
| 422 | - return null | ||
| 423 | - } | ||
| 424 | - | ||
| 425 | - const config = PLAN_TEMPLATES[formSn] | ||
| 426 | - if (!config) { | ||
| 427 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 428 | - return null | ||
| 429 | - } | ||
| 430 | - | ||
| 431 | - return config | ||
| 432 | -} | ||
| 433 | - | ||
| 434 | -/** | ||
| 435 | - * 获取币种符号 | ||
| 436 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 437 | - * @returns {string} 币种符号 | ||
| 438 | - * | ||
| 439 | - * @example | ||
| 440 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 441 | - */ | ||
| 442 | -export function getCurrencySymbol(currencyCode) { | ||
| 443 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 444 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | -} | ||
| 345 | - | ||
| 346 | -/** | ||
| 347 | - * 全局功能开关 | ||
| 348 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 349 | - * | ||
| 350 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 351 | - */ | ||
| 352 | -export const FEATURE_FLAGS = { | ||
| 353 | - /** | ||
| 354 | - * 多币种切换功能 | ||
| 355 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 356 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 357 | - * @type {boolean} | ||
| 358 | - */ | ||
| 359 | - MULTI_CURRENCY_ENABLED: false | ||
| 360 | -} | ||
| 361 | - | ||
| 362 | -/** | ||
| 363 | - * 币种符号映射 | ||
| 364 | - * @description 币种代码到符号的映射关系 | ||
| 365 | - */ | ||
| 366 | -export const CURRENCY_SYMBOLS = { | ||
| 367 | - CNY: '¥', // 人民币 | ||
| 368 | - USD: '$', // 美元 | ||
| 369 | - HKD: 'HK$', // 港币 | ||
| 370 | - EUR: '€' // 欧元 | ||
| 371 | -} | ||
| 372 | - | ||
| 373 | -/** | ||
| 374 | - * 币种完整信息映射 | ||
| 375 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 376 | - */ | ||
| 377 | -export const CURRENCY_MAP = { | ||
| 378 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 379 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 380 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 381 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 382 | -} | ||
| 383 | - | ||
| 384 | -/** | ||
| 385 | - * 根据 form_sn 获取模版配置 | ||
| 386 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 387 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 388 | - * | ||
| 389 | - * @example | ||
| 390 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 391 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 392 | - */ | ||
| 393 | -export function getTemplateConfig(formSn) { | ||
| 394 | - if (!formSn) { | ||
| 395 | - console.warn('[plan-templates] form_sn 为空') | ||
| 396 | - return null | ||
| 397 | - } | ||
| 398 | - | ||
| 399 | - const config = PLAN_TEMPLATES[formSn] | ||
| 400 | - if (!config) { | ||
| 401 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 402 | - return null | ||
| 403 | - } | ||
| 404 | - | ||
| 405 | - return config | ||
| 406 | -} | ||
| 407 | - | ||
| 408 | -/** | ||
| 409 | - * 获取币种符号 | ||
| 410 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 411 | - * @returns {string} 币种符号 | ||
| 412 | - * | ||
| 413 | - * @example | ||
| 414 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 415 | - */ | ||
| 416 | -export function getCurrencySymbol(currencyCode) { | ||
| 417 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 418 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | - | ||
| 345 | -} | ||
| 346 | - | ||
| 347 | -/** | ||
| 348 | - * 全局功能开关 | ||
| 349 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 350 | - * | ||
| 351 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 352 | - */ | ||
| 353 | -export const FEATURE_FLAGS = { | ||
| 354 | - /** | ||
| 355 | - * 多币种切换功能 | ||
| 356 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 357 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 358 | - * @type {boolean} | ||
| 359 | - */ | ||
| 360 | - MULTI_CURRENCY_ENABLED: false | ||
| 361 | -} | ||
| 362 | - | ||
| 363 | -/** | ||
| 364 | - * 币种符号映射 | ||
| 365 | - * @description 币种代码到符号的映射关系 | ||
| 366 | - */ | ||
| 367 | -export const CURRENCY_SYMBOLS = { | ||
| 368 | - CNY: '¥', // 人民币 | ||
| 369 | - USD: '$', // 美元 | ||
| 370 | - HKD: 'HK$', // 港币 | ||
| 371 | - EUR: '€' // 欧元 | ||
| 372 | -} | ||
| 373 | - | ||
| 374 | -/** | ||
| 375 | - * 币种完整信息映射 | ||
| 376 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 377 | - */ | ||
| 378 | -export const CURRENCY_MAP = { | ||
| 379 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 380 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 381 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 382 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 383 | -} | ||
| 384 | - | ||
| 385 | -/** | ||
| 386 | - * 根据 form_sn 获取模版配置 | ||
| 387 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 388 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 389 | - * | ||
| 390 | - * @example | ||
| 391 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 392 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 393 | - */ | ||
| 394 | -export function getTemplateConfig(formSn) { | ||
| 395 | - if (!formSn) { | ||
| 396 | - console.warn('[plan-templates] form_sn 为空') | ||
| 397 | - return null | ||
| 398 | - } | ||
| 399 | - | ||
| 400 | - const config = PLAN_TEMPLATES[formSn] | ||
| 401 | - if (!config) { | ||
| 402 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 403 | - return null | ||
| 404 | - } | ||
| 405 | - | ||
| 406 | - return config | ||
| 407 | -} | ||
| 408 | - | ||
| 409 | -/** | ||
| 410 | - * 获取币种符号 | ||
| 411 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 412 | - * @returns {string} 币种符号 | ||
| 413 | - * | ||
| 414 | - * @example | ||
| 415 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 416 | - */ | ||
| 417 | -export function getCurrencySymbol(currencyCode) { | ||
| 418 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 419 | -} |
| 1 | -/** | ||
| 2 | - * 计划书模版配置 | ||
| 3 | - * | ||
| 4 | - * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | - * @module config/plan-templates | ||
| 6 | - * @author Claude Code | ||
| 7 | - * @created 2026-02-06 | ||
| 8 | - * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | - * | ||
| 10 | - * --- 快速添加新产品(开发工具) --- | ||
| 11 | - * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | - * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | - * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | - * | ||
| 15 | - * 使用方式: | ||
| 16 | - * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | - * | ||
| 18 | - * --- 手动添加步骤 --- | ||
| 19 | - * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | - * 2. 复制现有配置作为模板 | ||
| 21 | - * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | - * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 23 | - */ | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * 计划书模版配置映射 | ||
| 27 | - * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 28 | - * | ||
| 29 | - * @example | ||
| 30 | - * // 产品 API 返回 | ||
| 31 | - * { | ||
| 32 | - * id: 1, | ||
| 33 | - * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 34 | - * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 35 | - * } | ||
| 36 | - */ | ||
| 37 | -// 基础提交字段映射(适用于人寿/重疾等通用表单) | ||
| 38 | -const baseSubmitMapping = { | ||
| 39 | - customer_name: { api_field: 'customer_name' }, | ||
| 40 | - gender: { api_field: 'customer_gender' }, | ||
| 41 | - birthday: { api_field: 'customer_birthday' }, | ||
| 42 | - smoker: { api_field: 'smoking_status' }, | ||
| 43 | - coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | ||
| 44 | - payment_period: { api_field: 'payment_years' }, | ||
| 45 | - total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' } | ||
| 46 | -} | ||
| 47 | - | ||
| 48 | -// 人寿/重疾基础表单 Schema(通用保障类) | ||
| 49 | -const protectionFormSchema = { | ||
| 50 | - base_fields: [ | ||
| 51 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 52 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 54 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 55 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' }, | ||
| 56 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 57 | - ] | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -// 储蓄类提交字段映射(在基础映射上追加提取计划字段) | ||
| 61 | -const savingsSubmitMapping = { | ||
| 62 | - ...baseSubmitMapping, | ||
| 63 | - withdrawal_enabled: { api_field: 'allow_reduce_amount' }, | ||
| 64 | - withdrawal_mode: { api_field: 'withdrawal_option' }, | ||
| 65 | - withdrawal_method: { api_field: 'withdrawal_method' }, | ||
| 66 | - annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' }, | ||
| 67 | - annual_increase_percentage: { api_field: 'annual_increase_percentage' }, | ||
| 68 | - withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' }, | ||
| 69 | - withdrawal_period_specified: { api_field: 'withdrawal_period' }, | ||
| 70 | - withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' }, | ||
| 71 | - withdrawal_period_fixed: { api_field: 'withdrawal_period' } | ||
| 72 | -} | ||
| 73 | - | ||
| 74 | -// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | ||
| 75 | -const savingsFormSchema = { | ||
| 76 | - // 基础字段:非提取计划部分 | ||
| 77 | - base_fields: [ | ||
| 78 | - { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | ||
| 79 | - { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | ||
| 80 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | ||
| 81 | - { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | ||
| 82 | - { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | ||
| 83 | - { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ||
| 84 | - ], | ||
| 85 | - // 提取计划字段:由 withdrawal_plan 开关控制 | ||
| 86 | - withdrawal_fields: [ | ||
| 87 | - { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | ||
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | ||
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 90 | - { 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: '指定提取金额' }] }, | ||
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | ||
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | ||
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | ||
| 96 | - ], | ||
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | ||
| 98 | - reset_map: { | ||
| 99 | - withdrawal_mode: { | ||
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | ||
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | -} | ||
| 105 | - | ||
| 106 | -export const PLAN_TEMPLATES = { | ||
| 107 | - // 人寿保险产品 - WIOP3E | ||
| 108 | - 'life-insurance-wiop3e': { | ||
| 109 | - name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 110 | - component: 'LifeInsuranceTemplate', | ||
| 111 | - config: { | ||
| 112 | - currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 113 | - payment_periods: [ | ||
| 114 | - // 缴费年期选项 | ||
| 115 | - '整付(0-75 岁)', | ||
| 116 | - '5 年(0-70 岁)', | ||
| 117 | - '10 年(0-70 岁)' | ||
| 118 | - ], | ||
| 119 | - age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 120 | - insurance_period: '终身', // 保险期间 | ||
| 121 | - form_schema: protectionFormSchema, | ||
| 122 | - submit_mapping: baseSubmitMapping | ||
| 123 | - } | ||
| 124 | - }, | ||
| 125 | - | ||
| 126 | - // 人寿保险产品 - WIOP3 | ||
| 127 | - 'life-insurance-wiop3': { | ||
| 128 | - name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 129 | - component: 'LifeInsuranceTemplate', | ||
| 130 | - config: { | ||
| 131 | - currency: 'USD', | ||
| 132 | - payment_periods: [ | ||
| 133 | - '整付(0-75 岁)', | ||
| 134 | - '5 年(0-70 岁)', | ||
| 135 | - '10 年(0-70 岁)' | ||
| 136 | - ], | ||
| 137 | - age_range: { min: 0, max: 75 }, | ||
| 138 | - insurance_period: '终身', | ||
| 139 | - form_schema: protectionFormSchema, | ||
| 140 | - submit_mapping: baseSubmitMapping | ||
| 141 | - } | ||
| 142 | - }, | ||
| 143 | - | ||
| 144 | - // 重疾保险产品 - MPC | ||
| 145 | - 'critical-illness-mpc': { | ||
| 146 | - name: 'MPC 守护无间重疾', | ||
| 147 | - component: 'CriticalIllnessTemplate', | ||
| 148 | - config: { | ||
| 149 | - currency: 'USD', | ||
| 150 | - payment_periods: [ | ||
| 151 | - '10 年(15 日 - 65 岁)', | ||
| 152 | - '20 年(15 日 - 65 岁)', | ||
| 153 | - '25 年(15 日 - 60 岁)' | ||
| 154 | - ], | ||
| 155 | - age_range: { min: 0, max: 65 }, | ||
| 156 | - insurance_period: '终身', | ||
| 157 | - form_schema: protectionFormSchema, | ||
| 158 | - submit_mapping: baseSubmitMapping | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | - | ||
| 162 | - // 重疾保险产品 - MBC PRO | ||
| 163 | - 'critical-illness-mbc-pro': { | ||
| 164 | - name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 165 | - component: 'CriticalIllnessTemplate', | ||
| 166 | - config: { | ||
| 167 | - currency: 'USD', | ||
| 168 | - payment_periods: [ | ||
| 169 | - '10 年(15 日 - 65 岁)', | ||
| 170 | - '20 年(15 日 - 65 岁)', | ||
| 171 | - '25 年(15 日 - 60 岁)' | ||
| 172 | - ], | ||
| 173 | - age_range: { min: 0, max: 65 }, | ||
| 174 | - insurance_period: '终身', | ||
| 175 | - form_schema: protectionFormSchema, | ||
| 176 | - submit_mapping: baseSubmitMapping | ||
| 177 | - } | ||
| 178 | - }, | ||
| 179 | - | ||
| 180 | - // 重疾保险产品 - MBC2 | ||
| 181 | - 'critical-illness-mbc2': { | ||
| 182 | - name: 'MBC2 活跃人生重疾保 2', | ||
| 183 | - component: 'CriticalIllnessTemplate', | ||
| 184 | - config: { | ||
| 185 | - currency: 'USD', | ||
| 186 | - payment_periods: [ | ||
| 187 | - '10 年(15 日 - 65 岁)', | ||
| 188 | - '20 年(15 日 - 65 岁)', | ||
| 189 | - '25 年(15 日 - 60 岁)' | ||
| 190 | - ], | ||
| 191 | - age_range: { min: 0, max: 65 }, | ||
| 192 | - insurance_period: '终身', | ||
| 193 | - form_schema: protectionFormSchema, | ||
| 194 | - submit_mapping: baseSubmitMapping | ||
| 195 | - } | ||
| 196 | - }, | ||
| 197 | - | ||
| 198 | - // ====== 储蓄型产品(统一逻辑) ====== | ||
| 199 | - | ||
| 200 | - // GS - 宏挚传承保障计划 | ||
| 201 | - 'savings-gs': { | ||
| 202 | - name: '宏挚传承保障计划', | ||
| 203 | - component: 'SavingsTemplate', | ||
| 204 | - category: 'savings', // 储蓄型产品 | ||
| 205 | - config: { | ||
| 206 | - currency: 'USD', // 默认美元 | ||
| 207 | - payment_periods: [ | ||
| 208 | - '整付', | ||
| 209 | - '3 年', | ||
| 210 | - '5 年', | ||
| 211 | - '10 年', | ||
| 212 | - '15 年', | ||
| 213 | - ], | ||
| 214 | - age_range: { min: 0, max: 100 }, | ||
| 215 | - insurance_period: '终身', | ||
| 216 | - // 提取计划配置 | ||
| 217 | - withdrawal_plan: { | ||
| 218 | - enabled: true, | ||
| 219 | - currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 220 | - default_currency: 'USD', // 统一为美元 | ||
| 221 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 222 | - withdrawal_periods: [ | ||
| 223 | - '1年', | ||
| 224 | - '2年', | ||
| 225 | - '3年', | ||
| 226 | - '5年', | ||
| 227 | - '10年', | ||
| 228 | - '15年', | ||
| 229 | - '20年', | ||
| 230 | - '终身' | ||
| 231 | - ] | ||
| 232 | - }, | ||
| 233 | - form_schema: savingsFormSchema, | ||
| 234 | - submit_mapping: savingsSubmitMapping | ||
| 235 | - } | ||
| 236 | - }, | ||
| 237 | - | ||
| 238 | - // GC - 宏挚家传保险计划 | ||
| 239 | - 'savings-gc': { | ||
| 240 | - name: '宏挚家传保险计划', | ||
| 241 | - component: 'SavingsTemplate', | ||
| 242 | - category: 'savings', | ||
| 243 | - config: { | ||
| 244 | - currency: 'USD', | ||
| 245 | - payment_periods: [ | ||
| 246 | - '整付', | ||
| 247 | - '3 年', | ||
| 248 | - '5 年', | ||
| 249 | - ], | ||
| 250 | - age_range: { min: 0, max: 100 }, | ||
| 251 | - insurance_period: '终身', | ||
| 252 | - withdrawal_plan: { | ||
| 253 | - enabled: true, | ||
| 254 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 255 | - default_currency: 'USD', // 统一为美元 | ||
| 256 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 257 | - withdrawal_periods: [ | ||
| 258 | - '1年', | ||
| 259 | - '2年', | ||
| 260 | - '3年', | ||
| 261 | - '5年', | ||
| 262 | - '10年', | ||
| 263 | - '15年', | ||
| 264 | - '20年', | ||
| 265 | - '终身' | ||
| 266 | - ] | ||
| 267 | - }, | ||
| 268 | - form_schema: savingsFormSchema, | ||
| 269 | - submit_mapping: savingsSubmitMapping | ||
| 270 | - } | ||
| 271 | - }, | ||
| 272 | - | ||
| 273 | - // FA - 宏浚传承保障计划 | ||
| 274 | - 'savings-fa': { | ||
| 275 | - name: '宏浚传承保障计划', | ||
| 276 | - component: 'SavingsTemplate', | ||
| 277 | - category: 'savings', | ||
| 278 | - config: { | ||
| 279 | - currency: 'USD', | ||
| 280 | - payment_periods: [ | ||
| 281 | - '整付', | ||
| 282 | - '2 年', | ||
| 283 | - '5 年', | ||
| 284 | - ], | ||
| 285 | - age_range: { min: 0, max: 100 }, | ||
| 286 | - insurance_period: '终身', | ||
| 287 | - withdrawal_plan: { | ||
| 288 | - enabled: true, | ||
| 289 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 290 | - default_currency: 'USD', // 统一为美元 | ||
| 291 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 292 | - withdrawal_periods: [ | ||
| 293 | - '1年', | ||
| 294 | - '2年', | ||
| 295 | - '3年', | ||
| 296 | - '5年', | ||
| 297 | - '10年', | ||
| 298 | - '15年', | ||
| 299 | - '20年', | ||
| 300 | - '终身' | ||
| 301 | - ] | ||
| 302 | - }, | ||
| 303 | - form_schema: savingsFormSchema, | ||
| 304 | - submit_mapping: savingsSubmitMapping | ||
| 305 | - } | ||
| 306 | - }, | ||
| 307 | - | ||
| 308 | - // LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险) | ||
| 309 | - 'savings-lv2': { | ||
| 310 | - name: '赤霞珠终身寿险计划2', | ||
| 311 | - component: 'SavingsTemplate', | ||
| 312 | - category: 'savings', | ||
| 313 | - config: { | ||
| 314 | - currency: 'USD', | ||
| 315 | - payment_periods: [ | ||
| 316 | - '5 年', | ||
| 317 | - '8 年', | ||
| 318 | - '12 年', | ||
| 319 | - '15 年', | ||
| 320 | - ], | ||
| 321 | - age_range: { min: 0, max: 100 }, | ||
| 322 | - insurance_period: '终身', | ||
| 323 | - withdrawal_plan: { | ||
| 324 | - enabled: true, | ||
| 325 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 326 | - default_currency: 'USD', // 统一为美元 | ||
| 327 | - withdrawal_modes: ['指定提取金额', '最高固定提取金额'], | ||
| 328 | - withdrawal_periods: [ | ||
| 329 | - '1年', | ||
| 330 | - '2年', | ||
| 331 | - '3年', | ||
| 332 | - '5年', | ||
| 333 | - '10年', | ||
| 334 | - '15年', | ||
| 335 | - '20年', | ||
| 336 | - '终身' | ||
| 337 | - ] | ||
| 338 | - }, | ||
| 339 | - form_schema: savingsFormSchema, | ||
| 340 | - submit_mapping: savingsSubmitMapping | ||
| 341 | - } | ||
| 342 | - }, | ||
| 343 | - | ||
| 344 | - /** | ||
| 345 | - * 测试计划书-智享未来2 | ||
| 346 | - * @added 2026-02-14T14:12:31.658Z | ||
| 347 | - * @source docs/to-parse/测试计划书-智享未来2.md | ||
| 348 | - */ | ||
| 349 | - 'savings-2-148b3acd': { | ||
| 350 | - name: '测试计划书-智享未来2', | ||
| 351 | - component: 'SavingsTemplate', | ||
| 352 | - category: 'savings', | ||
| 353 | - config: { | ||
| 354 | - currency: 'USD', | ||
| 355 | - payment_periods: ["整付","3年","5年"], | ||
| 356 | - age_range: { min: 0, max: 75 }, | ||
| 357 | - insurance_period: '终身', | ||
| 358 | - withdrawal_plan: { | ||
| 359 | - enabled: true, | ||
| 360 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 361 | - default_currency: 'USD', | ||
| 362 | - withdrawal_modes: ["年龄指定金额","最高固定金额"], | ||
| 363 | - withdrawal_periods: ["1年","3年","5年","10年"] | ||
| 364 | - }, | ||
| 365 | - form_schema: savingsFormSchema, | ||
| 366 | - submit_mapping: savingsSubmitMapping | ||
| 367 | - } | ||
| 368 | - }} | ||
| 369 | - | ||
| 370 | -/** | ||
| 371 | - * 全局功能开关 | ||
| 372 | - * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 373 | - * | ||
| 374 | - * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 375 | - */ | ||
| 376 | -export const FEATURE_FLAGS = { | ||
| 377 | - /** | ||
| 378 | - * 多币种切换功能 | ||
| 379 | - * @description false: 方案 1 - 固定币种(当前实现) | ||
| 380 | - * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 381 | - * @type {boolean} | ||
| 382 | - */ | ||
| 383 | - MULTI_CURRENCY_ENABLED: false | ||
| 384 | -} | ||
| 385 | - | ||
| 386 | -/** | ||
| 387 | - * 币种符号映射 | ||
| 388 | - * @description 币种代码到符号的映射关系 | ||
| 389 | - */ | ||
| 390 | -export const CURRENCY_SYMBOLS = { | ||
| 391 | - CNY: '¥', // 人民币 | ||
| 392 | - USD: '$', // 美元 | ||
| 393 | - HKD: 'HK$', // 港币 | ||
| 394 | - EUR: '€' // 欧元 | ||
| 395 | -} | ||
| 396 | - | ||
| 397 | -/** | ||
| 398 | - * 币种完整信息映射 | ||
| 399 | - * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 400 | - */ | ||
| 401 | -export const CURRENCY_MAP = { | ||
| 402 | - CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 403 | - USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 404 | - HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 405 | - EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 406 | -} | ||
| 407 | - | ||
| 408 | -/** | ||
| 409 | - * 根据 form_sn 获取模版配置 | ||
| 410 | - * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 411 | - * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 412 | - * | ||
| 413 | - * @example | ||
| 414 | - * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 415 | - * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 416 | - */ | ||
| 417 | -export function getTemplateConfig(formSn) { | ||
| 418 | - if (!formSn) { | ||
| 419 | - console.warn('[plan-templates] form_sn 为空') | ||
| 420 | - return null | ||
| 421 | - } | ||
| 422 | - | ||
| 423 | - const config = PLAN_TEMPLATES[formSn] | ||
| 424 | - if (!config) { | ||
| 425 | - console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 426 | - return null | ||
| 427 | - } | ||
| 428 | - | ||
| 429 | - return config | ||
| 430 | -} | ||
| 431 | - | ||
| 432 | -/** | ||
| 433 | - * 获取币种符号 | ||
| 434 | - * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 435 | - * @returns {string} 币种符号 | ||
| 436 | - * | ||
| 437 | - * @example | ||
| 438 | - * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 439 | - */ | ||
| 440 | -export function getCurrencySymbol(currencyCode) { | ||
| 441 | - return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 442 | -} |
docs/tasks/plan/字段条件显示系统扩展.md
0 → 100644
| 1 | +# 字段条件显示系统扩展计划 | ||
| 2 | + | ||
| 3 | +## 背景与目标 | ||
| 4 | + | ||
| 5 | +### 当前问题 | ||
| 6 | +- `show_when` 只支持简单的等于比较 (`equals`) | ||
| 7 | +- 不支持 OR 条件、嵌套条件 | ||
| 8 | +- 不支持不等于、大于、小于等操作符 | ||
| 9 | +- 隐藏字段的清理逻辑分散在 `reset_map` 中 | ||
| 10 | +- 提交时可能包含隐藏字段的脏数据 | ||
| 11 | + | ||
| 12 | +### 目标 | ||
| 13 | +构建一个声明式的条件规则系统,支持复杂逻辑同时保持配置可读性。 | ||
| 14 | + | ||
| 15 | +## 技术方案:方案 B - 条件规则引擎 | ||
| 16 | + | ||
| 17 | +### 核心文件变更 | ||
| 18 | + | ||
| 19 | +| 文件 | 操作 | 说明 | | ||
| 20 | +|------|------|------| | ||
| 21 | +| `src/config/plan-conditions.js` | 新建 | 条件操作符和评估引擎 | | ||
| 22 | +| `src/composables/useFieldDependencies.js` | 修改 | 集成新的条件评估器 | | ||
| 23 | +| `src/composables/usePlanSubmit.js` | 新建 | 提交时字段过滤逻辑 | | ||
| 24 | +| `src/config/plan-templates.js` | 修改 | 迁移现有配置到新格式 | | ||
| 25 | +| `src/composables/__tests__/plan-conditions.test.js` | 新建 | 条件引擎单元测试 | | ||
| 26 | + | ||
| 27 | +--- | ||
| 28 | + | ||
| 29 | +## 阶段 1:核心条件引擎 | ||
| 30 | + | ||
| 31 | +### 任务 1.1 创建条件操作符定义 | ||
| 32 | +- [x] 创建 `src/config/plan-conditions.js` | ||
| 33 | +- [x] 定义比较操作符:`eq`, `ne`, `gt`, `gte`, `lt`, `lte` | ||
| 34 | +- [x] 定义集合操作符:`in`, `nin` | ||
| 35 | +- [x] 定义字符串操作符:`contains`, `startsWith`, `matches` | ||
| 36 | +- [x] 定义布尔操作符:`truthy`, `falsy`, `empty`, `notEmpty` | ||
| 37 | +- [x] 编写单元测试验证操作符正确性 | ||
| 38 | + | ||
| 39 | +### 任务 1.2 实现条件评估函数 | ||
| 40 | +- [x] 实现 `evaluateCondition(condition, formData)` 函数 | ||
| 41 | +- [x] 支持简单条件:`{ field, op, value }` | ||
| 42 | +- [x] 支持 AND 逻辑:`{ and: [...] }` | ||
| 43 | +- [x] 支持 OR 逻辑:`{ or: [...] }` | ||
| 44 | +- [x] 支持 NOT 逻辑:`{ not: {...} }` | ||
| 45 | +- [x] 支持嵌套条件组合 | ||
| 46 | +- [x] 编写单元测试覆盖各种条件场景 | ||
| 47 | + | ||
| 48 | +### 任务 1.3 向后兼容处理 | ||
| 49 | +- [x] 支持旧格式 `show_when: { field: 'x', equals: 'y' }` | ||
| 50 | +- [x] 支持旧格式数组 `show_when: [{ field: 'x', equals: 'y' }]` | ||
| 51 | +- [x] 自动转换为新格式 | ||
| 52 | + | ||
| 53 | +--- | ||
| 54 | + | ||
| 55 | +## 阶段 2:清理机制 | ||
| 56 | + | ||
| 57 | +### 任务 2.1 字段清理规则 | ||
| 58 | +- [x] 在字段定义中添加 `clear_when_hidden` 属性 | ||
| 59 | +- [x] 实现 `clear_when_hidden: true` 自动清空 | ||
| 60 | +- [x] 实现 `clear_when_hidden: false` 保留值 | ||
| 61 | +- [x] 实现级联清理 `clear_when_hidden: { clear_dependents: [...] }` | ||
| 62 | + | ||
| 63 | +### 任务 2.2 提交时字段过滤 | ||
| 64 | +- [x] 实现 `filterHiddenFields(formData, visibleFields)` 函数 | ||
| 65 | +- [x] 只提交当前可见的字段 | ||
| 66 | +- [x] 保持 API 兼容性 | ||
| 67 | + | ||
| 68 | +### 任务 2.3 更新 useFieldDependencies | ||
| 69 | +- [x] 集成新的条件评估引擎 | ||
| 70 | +- [x] 实现字段隐藏时的自动清理 | ||
| 71 | +- [x] 更新 `isFieldVisible` 使用新引擎 | ||
| 72 | +- [x] 保持 API 兼容性 | ||
| 73 | + | ||
| 74 | +--- | ||
| 75 | + | ||
| 76 | +## 阶段 3:配置迁移 | ||
| 77 | + | ||
| 78 | +### 任务 3.1 迁移储蓄类模板 | ||
| 79 | +- [x] 迁移 `savingsFormSchema.withdrawal_fields` 到新格式 | ||
| 80 | +- [x] 删除 `reset_map`,使用 `clear_when_hidden` 替代 | ||
| 81 | +- [x] 验证功能正常(测试通过 160/160) | ||
| 82 | + | ||
| 83 | +### 任务 3.2 迁移保障类模板 | ||
| 84 | +- [x] 检查 `protectionFormSchema` 是否需要条件 | ||
| 85 | +- [x] 按需添加条件规则(保障类暂无条件需求,无需迁移) | ||
| 86 | + | ||
| 87 | +### 任务 3.3 更新文档解析工具 ⚠️ 重要 | ||
| 88 | +- [x] 更新 `src/utils/parsers/config-generator.js` 生成新格式配置 | ||
| 89 | +- [x] 修改 `show_when` 生成逻辑,使用 `{ field, op, value }` 格式 | ||
| 90 | +- [x] 停止生成 `reset_map`(已忽略) | ||
| 91 | +- [x] 测试通过(160/160) | ||
| 92 | + | ||
| 93 | +**说明**:MCP 文档解析服务保持不变,仅更新配置生成器输出格式。 | ||
| 94 | + | ||
| 95 | +### 任务 3.4 更新文档 | ||
| 96 | +- [x] 更新 `plan-templates.js` 顶部的使用说明 | ||
| 97 | +- [x] 添加条件规则配置示例(已在 plan-templates.js 注释中说明) | ||
| 98 | +- [x] 添加常见场景示例(已在 savingsFormSchema 中实现) | ||
| 99 | + | ||
| 100 | +--- | ||
| 101 | + | ||
| 102 | +## 阶段 4:测试与验证 | ||
| 103 | + | ||
| 104 | +### 任务 4.1 单元测试 | ||
| 105 | +- [ ] 条件操作符测试覆盖率 > 90% | ||
| 106 | +- [ ] 条件评估引擎测试覆盖率 > 90% | ||
| 107 | +- [ ] useFieldDependencies 测试更新 | ||
| 108 | + | ||
| 109 | +### 任务 4.2 集成测试 | ||
| 110 | +- [ ] 测试储蓄类产品完整流程 | ||
| 111 | +- [ ] 测试字段显示/隐藏切换 | ||
| 112 | +- [ ] 测试提交时字段过滤 | ||
| 113 | +- [ ] 测试向后兼容性 | ||
| 114 | + | ||
| 115 | +### 任务 4.3 真机验证 | ||
| 116 | +- [ ] 微信开发者工具验证 | ||
| 117 | +- [ ] 检查性能影响 | ||
| 118 | +- [ ] 检查内存占用 | ||
| 119 | + | ||
| 120 | +--- | ||
| 121 | + | ||
| 122 | +## 配置格式示例 | ||
| 123 | + | ||
| 124 | +### 新格式示例 | ||
| 125 | + | ||
| 126 | +```javascript | ||
| 127 | +// 简单条件 | ||
| 128 | +show_when: { field: 'smoker', op: 'eq', value: '是' } | ||
| 129 | + | ||
| 130 | +// 多条件 AND | ||
| 131 | +show_when: { | ||
| 132 | + and: [ | ||
| 133 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 134 | + { field: 'age', op: 'gte', value: 30 } | ||
| 135 | + ] | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +// OR 条件 | ||
| 139 | +show_when: { | ||
| 140 | + or: [ | ||
| 141 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 142 | + { field: 'age', op: 'gt', value: 50 } | ||
| 143 | + ] | ||
| 144 | +} | ||
| 145 | + | ||
| 146 | +// 嵌套条件 | ||
| 147 | +show_when: { | ||
| 148 | + and: [ | ||
| 149 | + { field: 'product_type', op: 'in', value: ['A', 'B'] }, | ||
| 150 | + { | ||
| 151 | + or: [ | ||
| 152 | + { field: 'coverage', op: 'gte', value: 100000 }, | ||
| 153 | + { field: 'payment_period', op: 'eq', value: '20年' } | ||
| 154 | + ] | ||
| 155 | + } | ||
| 156 | + ] | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +// 清理规则 | ||
| 160 | +{ | ||
| 161 | + id: 'withdrawal_amount', | ||
| 162 | + show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, | ||
| 163 | + clear_when_hidden: true // 隐藏时清空 | ||
| 164 | +} | ||
| 165 | +``` | ||
| 166 | + | ||
| 167 | +### 旧格式(保持兼容) | ||
| 168 | + | ||
| 169 | +```javascript | ||
| 170 | +// 旧格式仍然支持 | ||
| 171 | +show_when: { field: 'withdrawal_mode', equals: '指定提取金额' } | ||
| 172 | +show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] | ||
| 173 | +``` | ||
| 174 | + | ||
| 175 | +--- | ||
| 176 | + | ||
| 177 | +## 风险与注意事项 | ||
| 178 | + | ||
| 179 | +1. **向后兼容**:必须保持现有配置格式可用 | ||
| 180 | +2. **性能**:条件评估不能影响表单响应速度 | ||
| 181 | +3. **可读性**:配置文件需要保持可理解性 | ||
| 182 | +4. **测试覆盖**:每个操作符都需要充分测试 | ||
| 183 | + | ||
| 184 | +--- | ||
| 185 | + | ||
| 186 | +## 进度追踪 | ||
| 187 | + | ||
| 188 | +| 阶段 | 状态 | 完成时间 | | ||
| 189 | +|------|------|---------| | ||
| 190 | +| 阶段 1:核心引擎 | ✅ 完成 | 2026-02-15 | | ||
| 191 | +| 阶段 2:清理机制 | ✅ 完成 | 2026-02-15 | | ||
| 192 | +| 阶段 3:配置迁移 | ✅ 完成 | 2026-02-15 | | ||
| 193 | +| 阶段 4:测试验证 | 🔄 待真机验证 | - | | ||
| 194 | + | ||
| 195 | +--- | ||
| 196 | + | ||
| 197 | +**创建时间**: 2026-02-15 | ||
| 198 | +**预计工期**: 3-4 天 | ||
| 199 | +**维护者**: Claude Code |
| ... | @@ -9,8 +9,8 @@ | ... | @@ -9,8 +9,8 @@ |
| 9 | ## 📊 总体进度 | 9 | ## 📊 总体进度 |
| 10 | 10 | ||
| 11 | - [x] **第 1 步**: 目标与输出定义 | 11 | - [x] **第 1 步**: 目标与输出定义 |
| 12 | -- [ ] **第 2 步**: 文本抽取管线 | 12 | +- [x] **第 2 步**: 文本抽取管线 |
| 13 | -- [ ] **第 3 步**: 结构化解析与校验 | 13 | +- [x] **第 3 步**: 结构化解析与校验 |
| 14 | - [x] **第 4 步**: 生成与写入稳态化 | 14 | - [x] **第 4 步**: 生成与写入稳态化 |
| 15 | - [x] **第 5 步**: 测试与验证 | 15 | - [x] **第 5 步**: 测试与验证 |
| 16 | - [x] **第 6 步**: 运营与审计 | 16 | - [x] **第 6 步**: 运营与审计 | ... | ... |
| ... | @@ -29,15 +29,15 @@ | ... | @@ -29,15 +29,15 @@ |
| 29 | "postinstall": "weapp-tw patch", | 29 | "postinstall": "weapp-tw patch", |
| 30 | "lint": "eslint --ext .js,.vue src", | 30 | "lint": "eslint --ext .js,.vue src", |
| 31 | "test": "vitest run", | 31 | "test": "vitest run", |
| 32 | - "api:generate": "node scripts/generateApiFromOpenAPI.js", | 32 | + "api:generate": "node scripts/api-generator/generateApiFromOpenAPI.js", |
| 33 | - "changelog:check": "bash scripts/check-changelog.sh 7", | 33 | + "changelog:check": "bash scripts/changelog/check-changelog.sh 7", |
| 34 | - "changelog:check:30": "bash scripts/check-changelog.sh 30", | 34 | + "changelog:check:30": "bash scripts/changelog/check-changelog.sh 30", |
| 35 | - "changelog:check:all": "bash scripts/check-changelog.sh 0", | 35 | + "changelog:check:all": "bash scripts/changelog/check-changelog.sh 0", |
| 36 | "prepare": "husky", | 36 | "prepare": "husky", |
| 37 | - "parse:docs": "node scripts/parse-docs.js", | 37 | + "parse:docs": "node scripts/doc-parser/parse-docs.js", |
| 38 | - "parse:docs:list": "node scripts/parse-docs.js --list", | 38 | + "parse:docs:list": "node scripts/doc-parser/parse-docs.js --list", |
| 39 | - "parse:docs:status": "node scripts/parse-docs.js --status", | 39 | + "parse:docs:status": "node scripts/doc-parser/parse-docs.js --status", |
| 40 | - "parse:docs:file": "node scripts/parse-docs.js --file=", | 40 | + "parse:docs:file": "node scripts/doc-parser/parse-docs.js --file=", |
| 41 | "release": "standard-version" | 41 | "release": "standard-version" |
| 42 | }, | 42 | }, |
| 43 | "browserslist": [ | 43 | "browserslist": [ | ... | ... |
scripts/README.md
0 → 100644
| 1 | +# Scripts 目录说明 | ||
| 2 | + | ||
| 3 | +> 最后更新: 2026-02-15 | ||
| 4 | + | ||
| 5 | +本目录包含项目的各类开发工具脚本,按功能分组存放在子目录中。 | ||
| 6 | + | ||
| 7 | +## 📁 目录结构 | ||
| 8 | + | ||
| 9 | +``` | ||
| 10 | +scripts/ | ||
| 11 | +├── api-generator/ # API 代码生成工具 | ||
| 12 | +├── doc-parser/ # 文档解析工具 | ||
| 13 | +├── changelog/ # CHANGELOG 管理工具 | ||
| 14 | +└── README.md # 本说明文件 | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +--- | ||
| 18 | + | ||
| 19 | +## 🚀 快速使用 | ||
| 20 | + | ||
| 21 | +### API 代码生成 | ||
| 22 | +```bash | ||
| 23 | +pnpm api:generate # 从 OpenAPI 生成 API 接口 | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +### 文档解析 | ||
| 27 | +```bash | ||
| 28 | +pnpm parse:docs # 解析待处理文档 | ||
| 29 | +pnpm parse:docs:list # 查看待处理文档 | ||
| 30 | +pnpm parse:docs:status # 查看解析状态 | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +### CHANGELOG 检查 | ||
| 34 | +```bash | ||
| 35 | +pnpm changelog:check # 检查最近 7 天的漏记 | ||
| 36 | +pnpm changelog:check:30 # 检查最近 30 天的漏记 | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +--- | ||
| 40 | + | ||
| 41 | +## 📦 子目录详情 | ||
| 42 | + | ||
| 43 | +| 目录 | 用途 | 相关 npm scripts | | ||
| 44 | +|------|------|-----------------| | ||
| 45 | +| `api-generator/` | 从 OpenAPI 规范生成 JavaScript API 代码 | `api:generate` | | ||
| 46 | +| `doc-parser/` | 解析 PDF/DOCX 等文档,智能提取配置字段 | `parse:docs:*` | | ||
| 47 | +| `changelog/` | 检查和归档 CHANGELOG 记录 | `changelog:check:*` | | ||
| 48 | + | ||
| 49 | +--- | ||
| 50 | + | ||
| 51 | +## 🔗 相关文档 | ||
| 52 | + | ||
| 53 | +- [API 生成指南](./api-generator/GUIDE.md) | ||
| 54 | +- [文档解析快速开始](./doc-parser/QUICKSTART.md) | ||
| 55 | +- [CHANGELOG 规范](../docs/CHANGELOG.md) |
scripts/api-generator/README.md
0 → 100644
| 1 | +# API 代码生成工具 | ||
| 2 | + | ||
| 3 | +从 OpenAPI 规范自动生成 JavaScript API 接口代码。 | ||
| 4 | + | ||
| 5 | +## 📁 文件说明 | ||
| 6 | + | ||
| 7 | +| 文件 | 说明 | 使用频率 | | ||
| 8 | +|------|------|---------| | ||
| 9 | +| `generateApiFromOpenAPI.js` | 主脚本 - 从 OpenAPI 生成 API 代码 | ⭐⭐⭐ 高频 | | ||
| 10 | +| `apiDiff.js` | API 对比工具 - 检测 API 变更和破坏性改动 | ⭐⭐ 中频 | | ||
| 11 | +| `test-generate.js` | 测试脚本 - 验证生成的 API 文件是否正确 | ⭐ 低频 | | ||
| 12 | +| `API_GUIDE.md` | 使用指南 - 详细的 API 维护工作流文档 | 📖 文档 | | ||
| 13 | + | ||
| 14 | +## 🚀 使用方式 | ||
| 15 | + | ||
| 16 | +```bash | ||
| 17 | +# 生成所有 API 接口 | ||
| 18 | +pnpm api:generate | ||
| 19 | + | ||
| 20 | +# 生成指定模块 | ||
| 21 | +pnpm api:generate -- --module=user | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +## 📖 详细文档 | ||
| 25 | + | ||
| 26 | +参见 [API_GUIDE.md](./API_GUIDE.md) 了解完整的 API 维护工作流。 | ||
| 27 | + | ||
| 28 | +## 🔧 工作原理 | ||
| 29 | + | ||
| 30 | +1. 扫描 `docs/api-specs/` 目录下的 OpenAPI 文档 | ||
| 31 | +2. 解析 Markdown 文件中的 YAML 规范 | ||
| 32 | +3. 检测 API 变更(新增、修改、删除) | ||
| 33 | +4. 生成符合项目规范的 JavaScript API 代码 | ||
| 34 | +5. 保存到 `src/api/` 目录 | ||
| 35 | + | ||
| 36 | +## 📝 生成代码示例 | ||
| 37 | + | ||
| 38 | +输入 (OpenAPI YAML): | ||
| 39 | +```yaml | ||
| 40 | +paths: | ||
| 41 | + /user/info: | ||
| 42 | + get: | ||
| 43 | + summary: 获取用户信息 | ||
| 44 | + parameters: | ||
| 45 | + - name: user_id | ||
| 46 | + in: query | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +输出 (JavaScript): | ||
| 50 | +```javascript | ||
| 51 | +/** | ||
| 52 | + * 获取用户信息 | ||
| 53 | + * @param {Object} params | ||
| 54 | + * @param {string} params.user_id - 用户ID | ||
| 55 | + */ | ||
| 56 | +export const getUserInfoAPI = (params) => { | ||
| 57 | + return buildApiUrl('user_info', params) | ||
| 58 | +} | ||
| 59 | +``` |
scripts/changelog/README.md
0 → 100644
| 1 | +# CHANGELOG 管理工具 | ||
| 2 | + | ||
| 3 | +检查和归档项目的 CHANGELOG 记录。 | ||
| 4 | + | ||
| 5 | +## 📁 文件说明 | ||
| 6 | + | ||
| 7 | +| 文件 | 说明 | 使用频率 | | ||
| 8 | +|------|------|---------| | ||
| 9 | +| `check-changelog.sh` | 漏记检查 - 扫描 git 提交,检查 CHANGELOG 漏记 | ⭐⭐⭐ 高频 | | ||
| 10 | +| `archive-changelog.sh` | 归档脚本 - 当记录超过 20 条时自动归档 | ⭐ 低频 | | ||
| 11 | + | ||
| 12 | +## 🚀 使用方式 | ||
| 13 | + | ||
| 14 | +### 检查漏记 | ||
| 15 | + | ||
| 16 | +```bash | ||
| 17 | +# 检查最近 7 天(默认) | ||
| 18 | +pnpm changelog:check | ||
| 19 | + | ||
| 20 | +# 检查最近 30 天 | ||
| 21 | +pnpm changelog:check:30 | ||
| 22 | + | ||
| 23 | +# 检查所有提交 | ||
| 24 | +pnpm changelog:check:all | ||
| 25 | + | ||
| 26 | +# 直接运行 | ||
| 27 | +./scripts/changelog/check-changelog.sh 7 | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +### 归档旧记录 | ||
| 31 | + | ||
| 32 | +```bash | ||
| 33 | +# 直接运行归档脚本 | ||
| 34 | +./scripts/changelog/archive-changelog.sh | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +## 📋 检查输出示例 | ||
| 38 | + | ||
| 39 | +``` | ||
| 40 | +====================================== | ||
| 41 | + CHANGELOG 漏记检查工具 | ||
| 42 | +====================================== | ||
| 43 | + | ||
| 44 | +检查范围: 最近 7 天 | ||
| 45 | + | ||
| 46 | +[1/4] 正在获取 git 提交记录... | ||
| 47 | + 找到 5 个提交 | ||
| 48 | + | ||
| 49 | +[2/4] 正在读取 CHANGELOG 记录... | ||
| 50 | + 找到 3 条记录 | ||
| 51 | + | ||
| 52 | +[3/4] 正在对比... | ||
| 53 | + | ||
| 54 | +[4/4] 生成报告... | ||
| 55 | + | ||
| 56 | +⚠️ 发现 2 处漏记: | ||
| 57 | + | ||
| 58 | +1. feat(plan): 添加计划书表单验证 | ||
| 59 | + 提交: abc1234 (2026-02-14) | ||
| 60 | + 建议: 在 CHANGELOG 中添加此功能记录 | ||
| 61 | + | ||
| 62 | +2. fix(login): 修复登录跳转问题 | ||
| 63 | + 提交: def5678 (2026-02-13) | ||
| 64 | + 建议: 在 CHANGELOG 中添加此修复记录 | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +## 🔧 归档规则 | ||
| 68 | + | ||
| 69 | +当 `docs/CHANGELOG.md` 记录数超过 20 条时: | ||
| 70 | +1. 自动将旧记录移动到 `docs/changelog-archive/` | ||
| 71 | +2. 主文件只保留最近 20 条记录 | ||
| 72 | +3. 归档文件以日期命名:`CHANGELOG-archive-YYYYMMDD.md` | ||
| 73 | + | ||
| 74 | +## 📝 最佳实践 | ||
| 75 | + | ||
| 76 | +1. **每次提交后检查**:运行 `pnpm changelog:check` 确保没有漏记 | ||
| 77 | +2. **及时记录**:在提交代码时同步更新 CHANGELOG | ||
| 78 | +3. **定期归档**:每月运行一次归档脚本 |
scripts/doc-parser/README.md
0 → 100644
| 1 | +# 文档解析工具 | ||
| 2 | + | ||
| 3 | +从 PDF、DOCX 等文档中智能提取配置字段,自动生成计划书模板配置。 | ||
| 4 | + | ||
| 5 | +## 📁 文件说明 | ||
| 6 | + | ||
| 7 | +| 文件 | 说明 | 使用频率 | | ||
| 8 | +|------|------|---------| | ||
| 9 | +| `parse-docs.js` | 主脚本 - 文档解析和配置生成 | ⭐⭐⭐ 高频 | | ||
| 10 | +| `smart-field-extractor.js` | 智能字段提取器 - 从文档中提取表单字段 | ⭐⭐ 中频 | | ||
| 11 | +| `product-splitter.js` | 产品分割器 - 识别和分割多产品文档 | ⭐⭐ 中频 | | ||
| 12 | +| `parse-config.js` | 配置文件 - markitdown 和 AI 服务配置 | 📋 配置 | | ||
| 13 | +| `parse-docs.test.js` | 测试文件 - 单元测试 | 🧪 测试 | | ||
| 14 | +| `QUICKSTART.md` | 快速开始指南 | 📖 文档 | | ||
| 15 | + | ||
| 16 | +## 🚀 使用方式 | ||
| 17 | + | ||
| 18 | +```bash | ||
| 19 | +# 解析所有待处理文档 | ||
| 20 | +pnpm parse:docs | ||
| 21 | + | ||
| 22 | +# 查看待处理文档列表 | ||
| 23 | +pnpm parse:docs:list | ||
| 24 | + | ||
| 25 | +# 查看解析状态 | ||
| 26 | +pnpm parse:docs:status | ||
| 27 | + | ||
| 28 | +# 解析指定文件 | ||
| 29 | +pnpm parse:docs -- --file=产品说明书.pdf | ||
| 30 | + | ||
| 31 | +# 应用审核通过的配置 | ||
| 32 | +pnpm parse:docs -- --apply=计划书模版4 | ||
| 33 | + | ||
| 34 | +# 预览应用配置(不实际修改) | ||
| 35 | +pnpm parse:docs -- --apply=计划书模版4 --dry-run | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +## 📖 详细文档 | ||
| 39 | + | ||
| 40 | +参见 [QUICKSTART.md](./QUICKSTART.md) 了解完整的快速开始指南。 | ||
| 41 | + | ||
| 42 | +## 🔧 工作原理 | ||
| 43 | + | ||
| 44 | +1. 扫描 `docs/to-parse/` 目录下的待处理文档 | ||
| 45 | +2. 使用 markitdown 将文档转换为 Markdown | ||
| 46 | +3. 调用 AI 服务提取配置字段 | ||
| 47 | +4. 生成可审核的配置文件 | ||
| 48 | +5. 审核通过后应用到 `src/config/plan-templates.js` | ||
| 49 | + | ||
| 50 | +## 📝 支持的文档格式 | ||
| 51 | + | ||
| 52 | +- PDF (`.pdf`) | ||
| 53 | +- Word (`.doc`, `.docx`) | ||
| 54 | +- 文本 (`.txt`, `.md`) | ||
| 55 | + | ||
| 56 | +## 🤖 AI 配置 | ||
| 57 | + | ||
| 58 | +在 `.env` 文件中配置 AI 服务: | ||
| 59 | + | ||
| 60 | +```env | ||
| 61 | +# markitdown 服务 URL(可选) | ||
| 62 | +MARKITDOWN_URL=http://localhost:8000/convert | ||
| 63 | + | ||
| 64 | +# AI 服务配置(用于智能字段提取) | ||
| 65 | +AI_SERVICE_URL=your_ai_service_url | ||
| 66 | +AI_API_KEY=your_api_key | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## 📋 输出示例 | ||
| 70 | + | ||
| 71 | +解析后会生成: | ||
| 72 | +- `docs/parsed/产品名称.json` - 解析结果 | ||
| 73 | +- `docs/parsed/产品名称.audit.md` - 审核报告 |
| ... | @@ -15,6 +15,12 @@ | ... | @@ -15,6 +15,12 @@ |
| 15 | * | 15 | * |
| 16 | * # 查看待处理文档 | 16 | * # 查看待处理文档 |
| 17 | * npm run parse:docs -- --list | 17 | * npm run parse:docs -- --list |
| 18 | + * | ||
| 19 | + * # 应用审核通过的配置 | ||
| 20 | + * npm run parse:docs -- --apply=计划书模版4 | ||
| 21 | + * | ||
| 22 | + * # 预览应用配置(不实际修改) | ||
| 23 | + * npm run parse:docs -- --apply=计划书模版4 --dry-run | ||
| 18 | */ | 24 | */ |
| 19 | import crypto from 'crypto' | 25 | import crypto from 'crypto' |
| 20 | import fs from 'fs' | 26 | import fs from 'fs' |
| ... | @@ -976,17 +982,27 @@ ${code.trim()} | ... | @@ -976,17 +982,27 @@ ${code.trim()} |
| 976 | 982 | ||
| 977 | ## 📋 审核后操作 | 983 | ## 📋 审核后操作 |
| 978 | 984 | ||
| 979 | -### 确认无误 | 985 | +### 方法 1:自动应用(推荐) |
| 986 | +\`\`\`bash | ||
| 987 | +# 预览变更(不实际修改) | ||
| 988 | +pnpm parse:docs -- --apply=${baseFileName} --dry-run | ||
| 989 | + | ||
| 990 | +# 确认无误后,正式应用 | ||
| 991 | +pnpm parse:docs -- --apply=${baseFileName} | ||
| 992 | + | ||
| 993 | +# 说明: | ||
| 994 | +# 1. 自动提取配置代码并插入到 src/config/plan-templates.js | ||
| 995 | +# 2. 自动创建备份文件(docs/parsed-backup/) | ||
| 996 | +# 3. 自动将审核文件移动到 docs/parse-audit/approved/ | ||
| 997 | +\`\`\` | ||
| 998 | + | ||
| 999 | +### 方法 2:手动操作 | ||
| 980 | \`\`\`bash | 1000 | \`\`\`bash |
| 981 | # 1. 移动到 approved 目录 | 1001 | # 1. 移动到 approved 目录 |
| 982 | mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\ | 1002 | mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\ |
| 983 | docs/parse-audit/approved/ | 1003 | docs/parse-audit/approved/ |
| 984 | 1004 | ||
| 985 | -# 2. 合并到正式配置 | 1005 | +# 2. 手动复制"生成配置片段"到 src/config/plan-templates.js |
| 986 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 987 | - | ||
| 988 | -# 3. 删除待审核文件(可选) | ||
| 989 | -rm docs/parse-audit/pending/${baseFileName}/${auditFileName} | ||
| 990 | \`\`\` | 1006 | \`\`\` |
| 991 | 1007 | ||
| 992 | ### 需要修改 | 1008 | ### 需要修改 |
| ... | @@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) { | ... | @@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) { |
| 1402 | return true | 1418 | return true |
| 1403 | } | 1419 | } |
| 1404 | 1420 | ||
| 1421 | +/** | ||
| 1422 | + * 从审核文件应用配置到 plan-templates.js | ||
| 1423 | + * | ||
| 1424 | + * @description 读取审核 markdown 文件,提取配置代码,插入到配置文件中 | ||
| 1425 | + * @param {string} auditFileName - 审核文件名(不含路径,如 "计划书模版4") | ||
| 1426 | + * @param {Object} options - 选项 | ||
| 1427 | + * @param {boolean} options.dry_run - 是否仅预览 | ||
| 1428 | + * @returns {Object} 应用结果 | ||
| 1429 | + */ | ||
| 1430 | +function applyAuditFile(auditFileName, options = {}) { | ||
| 1431 | + const PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending') | ||
| 1432 | + const APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved') | ||
| 1433 | + | ||
| 1434 | + // 1. 查找审核文件 | ||
| 1435 | + let auditFile = null | ||
| 1436 | + let sourceDir = null | ||
| 1437 | + | ||
| 1438 | + // 先在 pending 目录查找 | ||
| 1439 | + const pendingDirs = fs.existsSync(PENDING_DIR) ? fs.readdirSync(PENDING_DIR) : [] | ||
| 1440 | + for (const dir of pendingDirs) { | ||
| 1441 | + const dirPath = path.join(PENDING_DIR, dir) | ||
| 1442 | + if (fs.statSync(dirPath).isDirectory()) { | ||
| 1443 | + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md')) | ||
| 1444 | + for (const file of files) { | ||
| 1445 | + // 匹配文件名或目录名 | ||
| 1446 | + const normalizedName = dir.replace(/\s+/g, '').toLowerCase() | ||
| 1447 | + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase() | ||
| 1448 | + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) { | ||
| 1449 | + auditFile = path.join(dirPath, file) | ||
| 1450 | + sourceDir = PENDING_DIR | ||
| 1451 | + break | ||
| 1452 | + } | ||
| 1453 | + } | ||
| 1454 | + } | ||
| 1455 | + if (auditFile) break | ||
| 1456 | + } | ||
| 1457 | + | ||
| 1458 | + // 如果 pending 没找到,在 approved 目录查找 | ||
| 1459 | + if (!auditFile && fs.existsSync(APPROVED_DIR)) { | ||
| 1460 | + const approvedFiles = fs.readdirSync(APPROVED_DIR).filter(f => f.endsWith('.md')) | ||
| 1461 | + for (const file of approvedFiles) { | ||
| 1462 | + // 从文件名提取产品名(格式:YYYY-MM-DD-产品名.md) | ||
| 1463 | + const match = file.match(/^\d{4}-\d{2}-\d{2}-(.+)\.md$/) | ||
| 1464 | + if (match) { | ||
| 1465 | + const normalizedName = match[1].replace(/\s+/g, '').toLowerCase() | ||
| 1466 | + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase() | ||
| 1467 | + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) { | ||
| 1468 | + auditFile = path.join(APPROVED_DIR, file) | ||
| 1469 | + sourceDir = APPROVED_DIR | ||
| 1470 | + break | ||
| 1471 | + } | ||
| 1472 | + } | ||
| 1473 | + } | ||
| 1474 | + } | ||
| 1475 | + | ||
| 1476 | + if (!auditFile) { | ||
| 1477 | + console.error("❌ 找不到审核文件: " + auditFileName) | ||
| 1478 | + console.log(" 搜索目录:") | ||
| 1479 | + console.log(" - docs/parse-audit/pending/") | ||
| 1480 | + console.log(" - docs/parse-audit/approved/") | ||
| 1481 | + return { ok: false, reason: 'file_not_found' } | ||
| 1482 | + } | ||
| 1483 | + | ||
| 1484 | + console.log("\n📄 找到审核文件: " + auditFile) | ||
| 1485 | + | ||
| 1486 | + // 2. 读取审核文件内容 | ||
| 1487 | + const content = fs.readFileSync(auditFile, 'utf-8') | ||
| 1488 | + | ||
| 1489 | + // 3. 提取配置代码片段 | ||
| 1490 | + const configMatch = content.match(/## 🧩 生成配置片段\s*\n+```javascript\s*\n([\s\S]*?)```/) | ||
| 1491 | + if (!configMatch) { | ||
| 1492 | + console.error("❌ 无法从审核文件中提取配置代码") | ||
| 1493 | + return { ok: false, reason: 'config_not_found' } | ||
| 1494 | + } | ||
| 1495 | + | ||
| 1496 | + const configCode = configMatch[1].trim() | ||
| 1497 | + console.log("\n📝 提取的配置代码:") | ||
| 1498 | + console.log("-".repeat(40)) | ||
| 1499 | + console.log(configCode) | ||
| 1500 | + console.log("-".repeat(40)) | ||
| 1501 | + | ||
| 1502 | + // 4. 提取 form_sn 用于去重检查 | ||
| 1503 | + const formSnMatch = configCode.match(/'([^']+)':\s*\{/) | ||
| 1504 | + const formSn = formSnMatch ? formSnMatch[1] : null | ||
| 1505 | + | ||
| 1506 | + if (!formSn) { | ||
| 1507 | + console.error("❌ 无法从配置代码中提取 form_sn") | ||
| 1508 | + return { ok: false, reason: 'form_sn_not_found' } | ||
| 1509 | + } | ||
| 1510 | + | ||
| 1511 | + console.log("\n🔑 form_sn: " + formSn) | ||
| 1512 | + | ||
| 1513 | + // 5. 读取现有配置文件 | ||
| 1514 | + const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8') | ||
| 1515 | + | ||
| 1516 | + // 检查是否已存在 | ||
| 1517 | + if (existingContent.includes(`'${formSn}':`)) { | ||
| 1518 | + console.error("❌ 配置文件中已存在 form_sn: " + formSn) | ||
| 1519 | + console.log(" 如需更新,请先手动删除旧配置") | ||
| 1520 | + return { ok: false, reason: 'duplicate', formSn } | ||
| 1521 | + } | ||
| 1522 | + | ||
| 1523 | + // 6. 找到插入位置(PLAN_TEMPLATES 对象的结束位置) | ||
| 1524 | + // 查找最后一个产品配置的结束位置 | ||
| 1525 | + const insertPattern = /(\n\s*'\w+[^']+':\s*\{[\s\S]*?\n\s*\}\s*,?\s*)(\n\})/ | ||
| 1526 | + const match = existingContent.match(insertPattern) | ||
| 1527 | + | ||
| 1528 | + if (!match) { | ||
| 1529 | + console.error("❌ 无法定位插入位置") | ||
| 1530 | + return { ok: false, reason: 'insert_not_found' } | ||
| 1531 | + } | ||
| 1532 | + | ||
| 1533 | + // 7. 构建新配置(确保有逗号) | ||
| 1534 | + let newConfigEntry = configCode | ||
| 1535 | + // 确保配置以逗号结尾 | ||
| 1536 | + if (!newConfigEntry.trimEnd().endsWith(',')) { | ||
| 1537 | + newConfigEntry = newConfigEntry.trimEnd() + ',' | ||
| 1538 | + } | ||
| 1539 | + | ||
| 1540 | + // 8. 插入配置 | ||
| 1541 | + const insertPosition = match.index + match[1].length | ||
| 1542 | + const updatedContent = | ||
| 1543 | + existingContent.slice(0, insertPosition) + | ||
| 1544 | + '\n\n' + | ||
| 1545 | + newConfigEntry + | ||
| 1546 | + existingContent.slice(insertPosition) | ||
| 1547 | + | ||
| 1548 | + if (options.dry_run) { | ||
| 1549 | + console.log("\n🧪 dry-run 模式,变更预览:") | ||
| 1550 | + console.log("-".repeat(40)) | ||
| 1551 | + console.log("将插入以下配置:") | ||
| 1552 | + console.log(newConfigEntry) | ||
| 1553 | + console.log("-".repeat(40)) | ||
| 1554 | + return { ok: true, dry_run: true, formSn } | ||
| 1555 | + } | ||
| 1556 | + | ||
| 1557 | + // 9. 备份并写入 | ||
| 1558 | + let backupFile = null | ||
| 1559 | + if (fs.existsSync(CONFIG_FILE)) { | ||
| 1560 | + ensureDir(BACKUP_DIR) | ||
| 1561 | + backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`) | ||
| 1562 | + fs.copyFileSync(CONFIG_FILE, backupFile) | ||
| 1563 | + console.log("\n💾 已备份到: " + backupFile) | ||
| 1564 | + } | ||
| 1565 | + | ||
| 1566 | + writeFile(CONFIG_FILE, updatedContent) | ||
| 1567 | + console.log("\n✅ 配置已更新: " + CONFIG_FILE) | ||
| 1568 | + | ||
| 1569 | + writeBackupLog({ | ||
| 1570 | + action: 'apply_audit', | ||
| 1571 | + backup_file: backupFile, | ||
| 1572 | + target_file: CONFIG_FILE, | ||
| 1573 | + audit_file: auditFile, | ||
| 1574 | + form_sn: formSn, | ||
| 1575 | + at: new Date().toISOString() | ||
| 1576 | + }) | ||
| 1577 | + | ||
| 1578 | + // 10. 移动审核文件到 approved 目录(如果是从 pending 来的) | ||
| 1579 | + if (sourceDir === PENDING_DIR) { | ||
| 1580 | + ensureDir(APPROVED_DIR) | ||
| 1581 | + const fileName = path.basename(auditFile) | ||
| 1582 | + const approvedPath = path.join(APPROVED_DIR, fileName) | ||
| 1583 | + | ||
| 1584 | + // 检查目标是否已存在 | ||
| 1585 | + if (fs.existsSync(approvedPath)) { | ||
| 1586 | + console.log("⚠️ approved 目录已存在同名文件,跳过移动") | ||
| 1587 | + } else { | ||
| 1588 | + fs.renameSync(auditFile, approvedPath) | ||
| 1589 | + console.log("📁 审核文件已移动到: " + approvedPath) | ||
| 1590 | + | ||
| 1591 | + // 删除空的 pending 子目录 | ||
| 1592 | + const pendingSubDir = path.dirname(auditFile) | ||
| 1593 | + const remainingFiles = fs.readdirSync(pendingSubDir).filter(f => !f.startsWith('.')) | ||
| 1594 | + if (remainingFiles.length === 0) { | ||
| 1595 | + fs.rmdirSync(pendingSubDir) | ||
| 1596 | + console.log("🗑️ 已删除空目录: " + pendingSubDir) | ||
| 1597 | + } | ||
| 1598 | + } | ||
| 1599 | + } | ||
| 1600 | + | ||
| 1601 | + return { ok: true, formSn, backupFile } | ||
| 1602 | +} | ||
| 1603 | + | ||
| 1405 | function updateConfigFile(newConfigs, options = {}) { | 1604 | function updateConfigFile(newConfigs, options = {}) { |
| 1406 | console.log("\n" + "=".repeat(60)) | 1605 | console.log("\n" + "=".repeat(60)) |
| 1407 | console.log("📝 更新配置文件: " + CONFIG_FILE) | 1606 | console.log("📝 更新配置文件: " + CONFIG_FILE) |
| ... | @@ -1563,9 +1762,16 @@ async function main() { | ... | @@ -1563,9 +1762,16 @@ async function main() { |
| 1563 | const listMode = args.includes('--list') | 1762 | const listMode = args.includes('--list') |
| 1564 | const fileMode = args.find(arg => arg.startsWith('--file=')) | 1763 | const fileMode = args.find(arg => arg.startsWith('--file=')) |
| 1565 | const writeMode = args.includes('--write-config') | 1764 | const writeMode = args.includes('--write-config') |
| 1566 | - const dryRunMode = args.includes('--dry-run') || !writeMode | ||
| 1567 | const rollbackMode = args.find(arg => arg.startsWith('--rollback=')) | 1765 | const rollbackMode = args.find(arg => arg.startsWith('--rollback=')) |
| 1568 | const statusMode = args.includes('--status') | 1766 | const statusMode = args.includes('--status') |
| 1767 | + const applyMode = args.find(arg => arg.startsWith('--apply=')) | ||
| 1768 | + | ||
| 1769 | + // dry-run 逻辑: | ||
| 1770 | + // 1. 如果显式指定 --dry-run,则 dry-run | ||
| 1771 | + // 2. 如果是 apply 模式,默认不 dry-run(除非显式指定) | ||
| 1772 | + // 3. 如果是解析模式,默认 dry-run(除非显式指定 --write-config) | ||
| 1773 | + const explicitDryRun = args.includes('--dry-run') | ||
| 1774 | + const dryRunMode = applyMode ? explicitDryRun : (!writeMode && !explicitDryRun || explicitDryRun) | ||
| 1569 | 1775 | ||
| 1570 | // 检查解析器选择 | 1776 | // 检查解析器选择 |
| 1571 | const parserModeArg = args.find(arg => arg.startsWith('--parser=')) | 1777 | const parserModeArg = args.find(arg => arg.startsWith('--parser=')) |
| ... | @@ -1586,6 +1792,11 @@ async function main() { | ... | @@ -1586,6 +1792,11 @@ async function main() { |
| 1586 | if (rollbackMode) { | 1792 | if (rollbackMode) { |
| 1587 | const backupFile = rollbackMode.split('=')[1] | 1793 | const backupFile = rollbackMode.split('=')[1] |
| 1588 | rollbackConfigFile(backupFile) | 1794 | rollbackConfigFile(backupFile) |
| 1795 | + } else if (applyMode) { | ||
| 1796 | + // 从审核文件应用配置 | ||
| 1797 | + const auditFileName = applyMode.split('=')[1] | ||
| 1798 | + const applyOptions = { dry_run: dryRunMode } | ||
| 1799 | + applyAuditFile(auditFileName, applyOptions) | ||
| 1589 | } else if (listMode) { | 1800 | } else if (listMode) { |
| 1590 | // 列出模式 | 1801 | // 列出模式 |
| 1591 | const docs = getDocsToParse() | 1802 | const docs = getDocsToParse() | ... | ... |
| ... | @@ -16,6 +16,7 @@ | ... | @@ -16,6 +16,7 @@ |
| 16 | * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 | 16 | * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 |
| 17 | * - FA 宏浚傳承保障計劃 | 17 | * - FA 宏浚傳承保障計劃 |
| 18 | * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項 | 18 | * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項 |
| 19 | + * - LV3 长宁終身壽險計劃3 | ||
| 19 | */ | 20 | */ |
| 20 | const PRODUCT_TITLE_PATTERNS = [ | 21 | const PRODUCT_TITLE_PATTERNS = [ |
| 21 | // 产品代码 + 产品名称 + 可选后缀 | 22 | // 产品代码 + 产品名称 + 可选后缀 |
| ... | @@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [ | ... | @@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [ |
| 31 | /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm, | 32 | /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm, |
| 32 | 33 | ||
| 33 | // 产品代码开头的行 | 34 | // 产品代码开头的行 |
| 34 | - /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm | 35 | + /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm, |
| 36 | + | ||
| 37 | + // 新增:产品代码 + 产品名称 + 数字后缀(如 "LV3 长宁終身壽險計劃3") | ||
| 38 | + /^([A-Z]{2,3}\d?)\s+([^\n]{2,25}?(?:計劃|计划|壽險|壽险)\d?)/gm | ||
| 35 | ] | 39 | ] |
| 36 | 40 | ||
| 37 | /** | 41 | /** |
| 38 | * 产品代码前缀列表(用于优先匹配) | 42 | * 产品代码前缀列表(用于优先匹配) |
| 39 | */ | 43 | */ |
| 40 | const PRODUCT_CODE_PREFIXES = [ | 44 | const PRODUCT_CODE_PREFIXES = [ |
| 41 | - 'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR', | 45 | + 'GS', 'GC', 'FA', 'LV2', 'LV3', 'LV', 'CR', 'HR', 'PR', 'SR', |
| 42 | 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR' | 46 | 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR' |
| 43 | ] | 47 | ] |
| 44 | 48 | ||
| ... | @@ -62,10 +66,11 @@ export function detectProductCount(content) { | ... | @@ -62,10 +66,11 @@ export function detectProductCount(content) { |
| 62 | export function findProductTitles(content) { | 66 | export function findProductTitles(content) { |
| 63 | const products = [] | 67 | const products = [] |
| 64 | const seenCodes = new Set() | 68 | const seenCodes = new Set() |
| 69 | + const seenNames = new Set() | ||
| 65 | 70 | ||
| 66 | // 策略1: 优先匹配产品代码前缀 | 71 | // 策略1: 优先匹配产品代码前缀 |
| 67 | for (const prefix of PRODUCT_CODE_PREFIXES) { | 72 | for (const prefix of PRODUCT_CODE_PREFIXES) { |
| 68 | - // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" | 73 | + // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 或 "LV3 长宁終身壽險計劃3" |
| 69 | const regex = new RegExp( | 74 | const regex = new RegExp( |
| 70 | `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`, | 75 | `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`, |
| 71 | 'gm' | 76 | 'gm' |
| ... | @@ -76,9 +81,11 @@ export function findProductTitles(content) { | ... | @@ -76,9 +81,11 @@ export function findProductTitles(content) { |
| 76 | const code = match[1] | 81 | const code = match[1] |
| 77 | const name = match[2].trim() | 82 | const name = match[2].trim() |
| 78 | 83 | ||
| 79 | - // 去重 | 84 | + // 去重(基于代码或名称) |
| 80 | - if (seenCodes.has(code)) continue | 85 | + const nameKey = name.replace(/\s+/g, '').toLowerCase() |
| 86 | + if (seenCodes.has(code) || seenNames.has(nameKey)) continue | ||
| 81 | seenCodes.add(code) | 87 | seenCodes.add(code) |
| 88 | + seenNames.add(nameKey) | ||
| 82 | 89 | ||
| 83 | products.push({ | 90 | products.push({ |
| 84 | index: match.index, | 91 | index: match.index, |
| ... | @@ -99,15 +106,52 @@ export function findProductTitles(content) { | ... | @@ -99,15 +106,52 @@ export function findProductTitles(content) { |
| 99 | const fullTitle = match[0].trim() | 106 | const fullTitle = match[0].trim() |
| 100 | if (fullTitle.length < 5) continue // 过滤太短的匹配 | 107 | if (fullTitle.length < 5) continue // 过滤太短的匹配 |
| 101 | 108 | ||
| 109 | + const code = match[1] || null | ||
| 110 | + const name = match[2] || fullTitle | ||
| 111 | + | ||
| 112 | + // 去重 | ||
| 113 | + const nameKey = name.replace(/\s+/g, '').toLowerCase() | ||
| 114 | + if (seenNames.has(nameKey)) continue | ||
| 115 | + if (code) seenCodes.add(code) | ||
| 116 | + seenNames.add(nameKey) | ||
| 117 | + | ||
| 102 | products.push({ | 118 | products.push({ |
| 103 | index: match.index, | 119 | index: match.index, |
| 104 | - code: match[1] || null, | 120 | + code, |
| 105 | - name: match[2] || fullTitle, | 121 | + name, |
| 106 | fullTitle | 122 | fullTitle |
| 107 | }) | 123 | }) |
| 108 | } | 124 | } |
| 109 | } | 125 | } |
| 110 | 126 | ||
| 127 | + // 策略3: 新增 - 识别包含"计划"但不包含产品代码的行(纯计划书名称) | ||
| 128 | + // 适用于标题如 "宏挚传承保障计划" 或 "长宁终身寿险计划3" | ||
| 129 | + if (products.length === 0) { | ||
| 130 | + const planNameRegex = /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/gm | ||
| 131 | + let match | ||
| 132 | + | ||
| 133 | + while ((match = planNameRegex.exec(content)) !== null) { | ||
| 134 | + const fullTitle = match[1].trim() | ||
| 135 | + | ||
| 136 | + // 排除太短或包含其他关键词的行 | ||
| 137 | + if (fullTitle.length < 5 || fullTitle.includes('選項') || fullTitle.includes('选项')) continue | ||
| 138 | + | ||
| 139 | + // 检查是否是产品名称(通常包含"保障"、"保险"、"寿险"等关键词) | ||
| 140 | + if (/(?:保障|保险|壽險|壽险|传承|家传)/.test(fullTitle)) { | ||
| 141 | + const nameKey = fullTitle.replace(/\s+/g, '').toLowerCase() | ||
| 142 | + if (!seenNames.has(nameKey)) { | ||
| 143 | + seenNames.add(nameKey) | ||
| 144 | + products.push({ | ||
| 145 | + index: match.index, | ||
| 146 | + code: null, | ||
| 147 | + name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明 | ||
| 148 | + fullTitle | ||
| 149 | + }) | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + | ||
| 111 | // 按出现位置排序 | 155 | // 按出现位置排序 |
| 112 | products.sort((a, b) => a.index - b.index) | 156 | products.sort((a, b) => a.index - b.index) |
| 113 | 157 | ... | ... |
| ... | @@ -8,6 +8,107 @@ | ... | @@ -8,6 +8,107 @@ |
| 8 | */ | 8 | */ |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | + * 保险缴费年期匹配模式 | ||
| 12 | + * | ||
| 13 | + * @description 用于识别各种格式的缴费年期选项 | ||
| 14 | + * @constant {Object} | ||
| 15 | + */ | ||
| 16 | +const PAYMENT_PERIOD_PATTERNS = { | ||
| 17 | + // 一次性缴费关键词 | ||
| 18 | + singlePayment: ['整付', '趸交', '躉繳', 'lump sum', 'single'], | ||
| 19 | + // 数字+年格式(3年、5年、10年等) | ||
| 20 | + yearlyPattern: /^(\d+)\s*年$/, | ||
| 21 | + // 至X岁格式 | ||
| 22 | + untilAgePattern: /^至?\s*(\d+)\s*岁$/, | ||
| 23 | + // 列表项格式(- 3年、• 5年等) | ||
| 24 | + listItemPattern: /^[-•·]\s*(.+)$/ | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * 检查是否为有效的缴费年期选项 | ||
| 29 | + * | ||
| 30 | + * @param {string} text - 待检查文本 | ||
| 31 | + * @returns {boolean} 是否为有效缴费年期 | ||
| 32 | + */ | ||
| 33 | +const isValidPaymentPeriod = (text) => { | ||
| 34 | + const trimmed = text.trim() | ||
| 35 | + | ||
| 36 | + // 排除无效关键字 | ||
| 37 | + if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) { | ||
| 38 | + return false | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // 1. 匹配 "X年" 格式 | ||
| 42 | + if (PAYMENT_PERIOD_PATTERNS.yearlyPattern.test(trimmed)) { | ||
| 43 | + return true | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + // 2. 匹配 "至X岁" 格式 | ||
| 47 | + if (PAYMENT_PERIOD_PATTERNS.untilAgePattern.test(trimmed)) { | ||
| 48 | + return true | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 3. 匹配一次性缴费关键词 | ||
| 52 | + if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw => | ||
| 53 | + trimmed.toLowerCase() === kw.toLowerCase() | ||
| 54 | + )) { | ||
| 55 | + return true | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // 4. 匹配列表项格式(提取内容后递归检查) | ||
| 59 | + const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern) | ||
| 60 | + if (listItemMatch) { | ||
| 61 | + return isValidPaymentPeriod(listItemMatch[1]) | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // 5. 通用匹配:包含"年"或"岁"或"交"的短文本(2-10字符) | ||
| 65 | + if (trimmed.length >= 2 && trimmed.length <= 10) { | ||
| 66 | + if (/年|岁|交|付/.test(trimmed)) { | ||
| 67 | + return true | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + return false | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +/** | ||
| 75 | + * 标准化缴费年期选项 | ||
| 76 | + * | ||
| 77 | + * @param {string} text - 原始文本 | ||
| 78 | + * @returns {string} 标准化后的文本 | ||
| 79 | + */ | ||
| 80 | +const normalizePaymentPeriod = (text) => { | ||
| 81 | + const trimmed = text.trim() | ||
| 82 | + | ||
| 83 | + // 标准化一次性缴费 | ||
| 84 | + if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw => | ||
| 85 | + trimmed.toLowerCase() === kw.toLowerCase() | ||
| 86 | + )) { | ||
| 87 | + return '整付' | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + // 标准化 "X年" 格式 | ||
| 91 | + const yearlyMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.yearlyPattern) | ||
| 92 | + if (yearlyMatch) { | ||
| 93 | + return `${yearlyMatch[1]}年` | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + // 标准化 "至X岁" 格式 | ||
| 97 | + const ageMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.untilAgePattern) | ||
| 98 | + if (ageMatch) { | ||
| 99 | + return `至${ageMatch[1]}岁` | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // 处理列表项格式 | ||
| 103 | + const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern) | ||
| 104 | + if (listItemMatch) { | ||
| 105 | + return normalizePaymentPeriod(listItemMatch[1]) | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + return trimmed | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +/** | ||
| 11 | * 字段提取规则配置 | 112 | * 字段提取规则配置 |
| 12 | * | 113 | * |
| 13 | * @description 定义每个字段的匹配规则、优先级和默认值 | 114 | * @description 定义每个字段的匹配规则、优先级和默认值 |
| ... | @@ -20,13 +121,30 @@ const FIELD_RULES = { | ... | @@ -20,13 +121,30 @@ const FIELD_RULES = { |
| 20 | /产品名称[::]\s*([^\n]+)/, | 121 | /产品名称[::]\s*([^\n]+)/, |
| 21 | /计划书名称[::]\s*([^\n]+)/, | 122 | /计划书名称[::]\s*([^\n]+)/, |
| 22 | /Product\s+Name[::]\s*([^\n]+)/i, | 123 | /Product\s+Name[::]\s*([^\n]+)/i, |
| 23 | - /^#\s+(.+)$/m // Markdown 标题 | 124 | + /^#\s+(.+)$/m, // Markdown 标题 |
| 125 | + // 新增:识别包含"计划"的标题行(如 "LV3 长宁終身壽險計劃3") | ||
| 126 | + // 格式:产品代码 + 中文产品名(包含"計劃/计划") | ||
| 127 | + /^[A-Z]{2,4}\d?\s*[-::]?\s*([^\n]*(?:計劃|计划)[^\n]*)/m, | ||
| 128 | + // 纯产品名称(包含"計劃/计划")- 通常为计划书名称 | ||
| 129 | + /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/m | ||
| 24 | ], | 130 | ], |
| 25 | fallback: null, // 必填,无默认值 | 131 | fallback: null, // 必填,无默认值 |
| 26 | - required: true | 132 | + required: true, |
| 133 | + postProcess: (value) => { | ||
| 134 | + // 处理正则匹配结果(数组) | ||
| 135 | + if (Array.isArray(value) && value.length > 1) { | ||
| 136 | + let name = value[1] || value[0] || '' | ||
| 137 | + // 移除产品代码前缀 | ||
| 138 | + name = name.replace(/^[A-Z]{2,4}\d?\s*[-::]?\s*/, '') | ||
| 139 | + // 移除后缀说明(如 "- 性別, 年齡, 出生年月日") | ||
| 140 | + name = name.split(/[-—::]/)[0].trim() | ||
| 141 | + return name || null | ||
| 142 | + } | ||
| 143 | + return value | ||
| 144 | + } | ||
| 27 | }, | 145 | }, |
| 28 | 146 | ||
| 29 | - // 产品类型 | 147 | + // 产品类型(保险类别) |
| 30 | product_type: { | 148 | product_type: { |
| 31 | priority: 2, | 149 | priority: 2, |
| 32 | patterns: [ | 150 | patterns: [ |
| ... | @@ -34,9 +152,21 @@ const FIELD_RULES = { | ... | @@ -34,9 +152,21 @@ const FIELD_RULES = { |
| 34 | { | 152 | { |
| 35 | type: 'content_match', | 153 | type: 'content_match', |
| 36 | rules: [ | 154 | rules: [ |
| 37 | - { keywords: ['储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' }, | 155 | + // 储蓄型产品(新增"储蓄产品"关键字) |
| 156 | + { keywords: ['储蓄产品', '储蓄型', '储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' }, | ||
| 157 | + // 重疾型产品 | ||
| 38 | { keywords: ['重疾', 'critical', '守护', '严重疾病'], value: 'critical-illness' }, | 158 | { keywords: ['重疾', 'critical', '守护', '严重疾病'], value: 'critical-illness' }, |
| 39 | - { keywords: ['人寿', 'life', '创富', '身故保障'], value: 'life-insurance' } | 159 | + // 人寿型产品 |
| 160 | + { keywords: ['人寿', 'life', '创富', '身故保障', '壽險', '壽险'], value: 'life-insurance' } | ||
| 161 | + ] | ||
| 162 | + }, | ||
| 163 | + // 新增:从标题行推断(如 "LV3 长宁終身壽險計劃3" 中的"壽險") | ||
| 164 | + { | ||
| 165 | + type: 'title_match', | ||
| 166 | + rules: [ | ||
| 167 | + { pattern: /終身壽險|終身寿险|壽險計劃|寿险计划/, value: 'life-insurance' }, | ||
| 168 | + { pattern: /儲蓄計劃|储蓄计划|儲蓄產品|储蓄产品/, value: 'savings' }, | ||
| 169 | + { pattern: /重疾|嚴重疾病/, value: 'critical-illness' } | ||
| 40 | ] | 170 | ] |
| 41 | } | 171 | } |
| 42 | ], | 172 | ], |
| ... | @@ -70,25 +200,12 @@ const FIELD_RULES = { | ... | @@ -70,25 +200,12 @@ const FIELD_RULES = { |
| 70 | priority: 4, | 200 | priority: 4, |
| 71 | patterns: [ | 201 | patterns: [ |
| 72 | // 匹配 "年繳保費繳費年期" 或 "缴费年期" 后面的列表 | 202 | // 匹配 "年繳保費繳費年期" 或 "缴费年期" 后面的列表 |
| 73 | - // 策略:匹配到包含 "年" 或 "整付" 的所有行,直到遇到其他关键字 | 203 | + // 策略:使用通用匹配函数识别各种格式的缴费年期选项 |
| 74 | { | 204 | { |
| 75 | type: 'smart_list_extract', | 205 | type: 'smart_list_extract', |
| 76 | startPattern: /(?:年繳保費)?繳費年期[::\s]*\n/, | 206 | startPattern: /(?:年繳保費)?繳費年期[::\s]*\n/, |
| 77 | endKeywords: ['提取', '保險期間', '保险期间', '投保年龄', '投保年齡', '選是', '選項', 'GC宏', 'FA宏', 'LV2'], | 207 | endKeywords: ['提取', '保險期間', '保险期间', '投保年龄', '投保年齡', '選是', '選項', 'GC宏', 'FA宏', 'LV2'], |
| 78 | - itemFilter: (line) => { | 208 | + itemFilter: (line) => isValidPaymentPeriod(line) |
| 79 | - const trimmed = line.trim() | ||
| 80 | - // 排除包含"投保年龄"等关键字的行 | ||
| 81 | - if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) { | ||
| 82 | - return false | ||
| 83 | - } | ||
| 84 | - // 精确匹配 "整付" 或 "X年" 格式 | ||
| 85 | - return trimmed && ( | ||
| 86 | - /^\d+\s*年$/.test(trimmed) || | ||
| 87 | - trimmed === '整付' || | ||
| 88 | - /^\d+年$/.test(trimmed) || | ||
| 89 | - /^[-•·]\s*\d+\s*年$/.test(trimmed) // 支持列表格式 "- 3年" | ||
| 90 | - ) | ||
| 91 | - } | ||
| 92 | } | 209 | } |
| 93 | ], | 210 | ], |
| 94 | fallback: ['整付', '3年', '5年'], | 211 | fallback: ['整付', '3年', '5年'], |
| ... | @@ -96,24 +213,17 @@ const FIELD_RULES = { | ... | @@ -96,24 +213,17 @@ const FIELD_RULES = { |
| 96 | postProcess: (values) => { | 213 | postProcess: (values) => { |
| 97 | // 过滤并标准化 | 214 | // 过滤并标准化 |
| 98 | const normalized = values | 215 | const normalized = values |
| 99 | - .map(v => v.trim()) | 216 | + .map(v => normalizePaymentPeriod(v)) |
| 100 | - // 排除包含"投保"等无效关键字 | 217 | + .filter(v => v && isValidPaymentPeriod(v)) |
| 101 | - .filter(v => v && !v.includes('投保') && !v.includes('年龄') && !v.includes('年齡')) | ||
| 102 | - .filter(v => v.includes('年') || v.includes('整付')) | ||
| 103 | - .map(v => { | ||
| 104 | - // 提取数字+年格式 | ||
| 105 | - const match = v.match(/(\d+)\s*年|整付/i) | ||
| 106 | - if (match) { | ||
| 107 | - return match[0].includes('整付') ? '整付' : `${match[1]}年` | ||
| 108 | - } | ||
| 109 | - return v | ||
| 110 | - }) | ||
| 111 | 218 | ||
| 112 | - // 去重、排序 | 219 | + // 去重、排序(整付放最前,其他按数字排序) |
| 113 | return [...new Set(normalized)].sort((a, b) => { | 220 | return [...new Set(normalized)].sort((a, b) => { |
| 114 | if (a === '整付') return -1 | 221 | if (a === '整付') return -1 |
| 115 | if (b === '整付') return 1 | 222 | if (b === '整付') return 1 |
| 116 | - return parseInt(a) - parseInt(b) | 223 | + // 提取数字进行排序 |
| 224 | + const numA = parseInt(a.match(/\d+/)?.[0] || '999') | ||
| 225 | + const numB = parseInt(b.match(/\d+/)?.[0] || '999') | ||
| 226 | + return numA - numB | ||
| 117 | }) | 227 | }) |
| 118 | } | 228 | } |
| 119 | }, | 229 | }, |
| ... | @@ -252,6 +362,11 @@ function extractField(content, fieldName) { | ... | @@ -252,6 +362,11 @@ function extractField(content, fieldName) { |
| 252 | patternDesc = `content_match(${pattern.rules.length} rules)` | 362 | patternDesc = `content_match(${pattern.rules.length} rules)` |
| 253 | break | 363 | break |
| 254 | 364 | ||
| 365 | + case 'title_match': | ||
| 366 | + match = matchByTitle(content, pattern.rules) | ||
| 367 | + patternDesc = `title_match(${pattern.rules.length} rules)` | ||
| 368 | + break | ||
| 369 | + | ||
| 255 | case 'count_match': | 370 | case 'count_match': |
| 256 | match = matchByCount(content, pattern.rules) | 371 | match = matchByCount(content, pattern.rules) |
| 257 | patternDesc = `count_match(${pattern.rules.length} rules)` | 372 | patternDesc = `count_match(${pattern.rules.length} rules)` |
| ... | @@ -276,6 +391,11 @@ function extractField(content, fieldName) { | ... | @@ -276,6 +391,11 @@ function extractField(content, fieldName) { |
| 276 | match = extractRange(content, pattern.pattern) | 391 | match = extractRange(content, pattern.pattern) |
| 277 | patternDesc = `range_extract` | 392 | patternDesc = `range_extract` |
| 278 | break | 393 | break |
| 394 | + | ||
| 395 | + case 'options_extract': | ||
| 396 | + match = extractOptionsFields(content, pattern.keyword) | ||
| 397 | + patternDesc = `options_extract(${pattern.keyword})` | ||
| 398 | + break | ||
| 279 | } | 399 | } |
| 280 | } else if (pattern instanceof RegExp) { | 400 | } else if (pattern instanceof RegExp) { |
| 281 | // 正则表达式匹配 | 401 | // 正则表达式匹配 |
| ... | @@ -331,6 +451,120 @@ function matchByContent(content, rules) { | ... | @@ -331,6 +451,120 @@ function matchByContent(content, rules) { |
| 331 | } | 451 | } |
| 332 | 452 | ||
| 333 | /** | 453 | /** |
| 454 | + * 通过标题模式匹配 | ||
| 455 | + * | ||
| 456 | + * @description 从文档标题或产品名称行中匹配产品类型 | ||
| 457 | + * @param {string} content - 文档内容 | ||
| 458 | + * @param {Array} rules - 匹配规则数组 | ||
| 459 | + * @returns {string|null} 匹配的值 | ||
| 460 | + */ | ||
| 461 | +function matchByTitle(content, rules) { | ||
| 462 | + // 提取可能的产品标题行(通常在文档开头) | ||
| 463 | + const lines = content.split('\n').slice(0, 20) // 只检查前20行 | ||
| 464 | + const titleLines = lines.filter(line => { | ||
| 465 | + const trimmed = line.trim() | ||
| 466 | + // 标题行通常包含产品代码、产品名称等 | ||
| 467 | + return trimmed.length > 5 && trimmed.length < 100 && | ||
| 468 | + (/[A-Z]{2,4}\d?/.test(trimmed) || | ||
| 469 | + /計劃|计划|保障|保险|壽險|壽险/.test(trimmed)) | ||
| 470 | + }) | ||
| 471 | + | ||
| 472 | + // 合并标题行进行匹配 | ||
| 473 | + const titleText = titleLines.join('\n') | ||
| 474 | + | ||
| 475 | + for (const rule of rules) { | ||
| 476 | + if (rule.pattern && rule.pattern.test(titleText)) { | ||
| 477 | + return rule.value | ||
| 478 | + } | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + return null | ||
| 482 | +} | ||
| 483 | + | ||
| 484 | +/** | ||
| 485 | + * 从"选项"段落提取字段 | ||
| 486 | + * | ||
| 487 | + * @description 识别 "XXX選項:" 或 "XXX选项:" 格式的段落,提取其中的字段列表 | ||
| 488 | + * 示例: | ||
| 489 | + * - "基本人壽保障選項:" 之后的性别、年龄等字段 | ||
| 490 | + * - "提取選項:" 之后的提取方式 | ||
| 491 | + * @param {string} content - 文档内容 | ||
| 492 | + * @param {string} optionKeyword - 选项关键词(如 "基本人壽保障選項") | ||
| 493 | + * @returns {Array<{name: string, description: string}>|null} 提取的字段列表 | ||
| 494 | + */ | ||
| 495 | +function extractOptionsFields(content, optionKeyword) { | ||
| 496 | + // 匹配 "XXX選項:" 或 "XXX选项:" 格式 | ||
| 497 | + const pattern = new RegExp(`${optionKeyword}[::\\s]*\\n([\\s\\S]*?)(?=\\n\\n|\\n[A-Z]|\\n\\d+\\.\\s|$)`, 'gm') | ||
| 498 | + const match = content.match(pattern) | ||
| 499 | + | ||
| 500 | + if (!match) return null | ||
| 501 | + | ||
| 502 | + const optionContent = match[0] | ||
| 503 | + const fields = [] | ||
| 504 | + | ||
| 505 | + // 按行分割,提取字段名和描述 | ||
| 506 | + const lines = optionContent.split('\n').slice(1) // 跳过标题行 | ||
| 507 | + | ||
| 508 | + for (const line of lines) { | ||
| 509 | + const trimmed = line.trim() | ||
| 510 | + if (!trimmed) continue | ||
| 511 | + | ||
| 512 | + // 常见字段格式: | ||
| 513 | + // 1. "性別" - 纯字段名 | ||
| 514 | + // 2. "年齡 (1-75岁)" - 字段名 + 范围说明 | ||
| 515 | + // 3. "- 出生年月日" - 列表格式 | ||
| 516 | + // 4. "提取金额: 指定金额" - 字段名 + 描述 | ||
| 517 | + | ||
| 518 | + // 提取字段名(去除列表符号和说明) | ||
| 519 | + const fieldMatch = trimmed.match(/^(?:[-•·]\s*)?([^::((]+)/) | ||
| 520 | + if (fieldMatch) { | ||
| 521 | + const fieldName = fieldMatch[1].trim() | ||
| 522 | + if (fieldName && fieldName.length > 0 && fieldName.length < 20) { | ||
| 523 | + fields.push({ | ||
| 524 | + name: fieldName, | ||
| 525 | + description: trimmed // 保留完整描述 | ||
| 526 | + }) | ||
| 527 | + } | ||
| 528 | + } | ||
| 529 | + } | ||
| 530 | + | ||
| 531 | + return fields.length > 0 ? fields : null | ||
| 532 | +} | ||
| 533 | + | ||
| 534 | +/** | ||
| 535 | + * 查找所有"选项"段落 | ||
| 536 | + * | ||
| 537 | + * @description 扫描文档中所有包含"選項"或"选项"的段落标题 | ||
| 538 | + * @param {string} content - 文档内容 | ||
| 539 | + * @returns {Array<{keyword: string, startIndex: number, fields: Array}>} 选项段落列表 | ||
| 540 | + */ | ||
| 541 | +function findAllOptionSections(content) { | ||
| 542 | + const sections = [] | ||
| 543 | + | ||
| 544 | + // 匹配所有 "XXX選項:" 或 "XXX选项:" 格式 | ||
| 545 | + const pattern = /([^\n]*(?:選項|选项|選是)[::]?)/gm | ||
| 546 | + let match | ||
| 547 | + | ||
| 548 | + while ((match = pattern.exec(content)) !== null) { | ||
| 549 | + const keyword = match[1].trim() | ||
| 550 | + const startIndex = match.index | ||
| 551 | + | ||
| 552 | + // 提取该选项下的字段 | ||
| 553 | + const fields = extractOptionsFields(content, keyword.replace(/[::]/g, '')) | ||
| 554 | + | ||
| 555 | + if (fields && fields.length > 0) { | ||
| 556 | + sections.push({ | ||
| 557 | + keyword, | ||
| 558 | + startIndex, | ||
| 559 | + fields | ||
| 560 | + }) | ||
| 561 | + } | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + return sections | ||
| 565 | +} | ||
| 566 | + | ||
| 567 | +/** | ||
| 334 | * 通过统计匹配内容 | 568 | * 通过统计匹配内容 |
| 335 | */ | 569 | */ |
| 336 | function matchByCount(content, rules) { | 570 | function matchByCount(content, rules) { | ... | ... |
scripts/整理报告.md
0 → 100644
| 1 | +# Scripts 目录整理报告 | ||
| 2 | + | ||
| 3 | +> 整理日期: 2026-02-15 | ||
| 4 | + | ||
| 5 | +## ✅ 整理完成 | ||
| 6 | + | ||
| 7 | +### 新目录结构 | ||
| 8 | + | ||
| 9 | +``` | ||
| 10 | +scripts/ | ||
| 11 | +├── README.md # 主说明文档 | ||
| 12 | +├── CLAUDE.md # Claude Code 指南(待处理) | ||
| 13 | +│ | ||
| 14 | +├── api-generator/ # API 代码生成工具 | ||
| 15 | +│ ├── README.md # 使用说明 | ||
| 16 | +│ ├── GUIDE.md # 详细指南 | ||
| 17 | +│ ├── generateApiFromOpenAPI.js # 主脚本 ⭐ | ||
| 18 | +│ ├── apiDiff.js # API 对比工具 | ||
| 19 | +│ └── test-generate.js # 测试脚本 | ||
| 20 | +│ | ||
| 21 | +├── doc-parser/ # 文档解析工具 | ||
| 22 | +│ ├── README.md # 使用说明 | ||
| 23 | +│ ├── QUICKSTART.md # 快速开始 | ||
| 24 | +│ ├── .env.example # 环境变量示例 | ||
| 25 | +│ ├── parse-docs.js # 主脚本 ⭐ | ||
| 26 | +│ ├── parse-config.js # 配置文件 | ||
| 27 | +│ ├── smart-field-extractor.js # 字段提取器 | ||
| 28 | +│ ├── product-splitter.js # 产品分割器 | ||
| 29 | +│ └── parse-docs.test.js # 测试文件 | ||
| 30 | +│ | ||
| 31 | +└── changelog/ # CHANGELOG 管理工具 | ||
| 32 | + ├── README.md # 使用说明 | ||
| 33 | + ├── check-changelog.sh # 漏记检查 ⭐ | ||
| 34 | + └── archive-changelog.sh # 归档脚本 | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +### npm scripts 已更新 | ||
| 38 | + | ||
| 39 | +| 命令 | 新路径 | | ||
| 40 | +|------|--------| | ||
| 41 | +| `pnpm api:generate` | `scripts/api-generator/generateApiFromOpenAPI.js` | | ||
| 42 | +| `pnpm parse:docs` | `scripts/doc-parser/parse-docs.js` | | ||
| 43 | +| `pnpm changelog:check` | `scripts/changelog/check-changelog.sh` | | ||
| 44 | + | ||
| 45 | +--- | ||
| 46 | + | ||
| 47 | +## ⚠️ 未使用文件列表 | ||
| 48 | + | ||
| 49 | +以下文件**没有在 package.json 或其他脚本中直接调用**,请判断是否需要保留: | ||
| 50 | + | ||
| 51 | +### 1. `scripts/changelog/archive-changelog.sh` | ||
| 52 | + | ||
| 53 | +**状态**: 🟡 未直接引用 | ||
| 54 | +**功能**: CHANGELOG 归档脚本,当记录超过 20 条时自动归档 | ||
| 55 | +**使用方式**: 手动运行 `./scripts/changelog/archive-changelog.sh` | ||
| 56 | +**建议**: | ||
| 57 | +- [ ] 保留 - 作为手动维护工具 | ||
| 58 | +- [ ] 删除 - 不再需要归档功能 | ||
| 59 | + | ||
| 60 | +--- | ||
| 61 | + | ||
| 62 | +### 2. `scripts/api-generator/test-generate.js` | ||
| 63 | + | ||
| 64 | +**状态**: 🟡 未直接引用 | ||
| 65 | +**功能**: 测试生成的 API 文件是否正确 | ||
| 66 | +**使用方式**: 手动运行 `node scripts/api-generator/test-generate.js` | ||
| 67 | +**建议**: | ||
| 68 | +- [ ] 保留 - 作为开发调试工具 | ||
| 69 | +- [ ] 删除 - 已有其他测试方式 | ||
| 70 | + | ||
| 71 | +--- | ||
| 72 | + | ||
| 73 | +### 3. `scripts/CLAUDE.md` | ||
| 74 | + | ||
| 75 | +**状态**: 🟡 需要评估 | ||
| 76 | +**功能**: 给 Claude Code 的说明文档 | ||
| 77 | +**位置**: 当前在 scripts 根目录 | ||
| 78 | +**建议**: | ||
| 79 | +- [ ] 保留在当前位置 - 作为 Claude 指南 | ||
| 80 | +- [ ] 移动到 doc-parser 目录 - 因为主要与文档解析相关 | ||
| 81 | +- [ ] 删除 - 内容已整合到其他文档 | ||
| 82 | + | ||
| 83 | +--- | ||
| 84 | + | ||
| 85 | +## 📝 整理后的改进 | ||
| 86 | + | ||
| 87 | +1. **✅ 功能分组清晰** - 按用途分为 3 个子目录 | ||
| 88 | +2. **✅ 每个子目录有 README** - 包含使用说明和文件介绍 | ||
| 89 | +3. **✅ 路径引用已更新** - package.json 已同步更新 | ||
| 90 | +4. **✅ 单个文件已收录** - 所有脚本都在对应的功能目录中 | ||
| 91 | + | ||
| 92 | +--- | ||
| 93 | + | ||
| 94 | +## 🔧 后续操作建议 | ||
| 95 | + | ||
| 96 | +1. **确认未使用文件** - 对上述 3 个文件做出保留/删除决定 | ||
| 97 | +2. **更新 CLAUDE.md** - 如果项目根目录的 CLAUDE.md 已包含相关内容,可以删除 scripts/CLAUDE.md | ||
| 98 | +3. **测试脚本** - 运行 `pnpm api:generate` 和 `pnpm parse:docs` 确保路径更新正确 |
| ... | @@ -5,10 +5,16 @@ | ... | @@ -5,10 +5,16 @@ |
| 5 | * @module composables/useFieldDependencies | 5 | * @module composables/useFieldDependencies |
| 6 | * @author Claude Code | 6 | * @author Claude Code |
| 7 | * @created 2026-02-14 | 7 | * @created 2026-02-14 |
| 8 | + * @updated 2026-02-15 - 集成新的条件评估引擎,支持复杂条件 | ||
| 8 | */ | 9 | */ |
| 9 | 10 | ||
| 10 | -import { computed, reactive, isRef } from 'vue' | 11 | +import { computed, reactive, isRef, watch } from 'vue' |
| 11 | import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields' | 12 | import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields' |
| 13 | +import { | ||
| 14 | + evaluateCondition, | ||
| 15 | + getConditionDependencies, | ||
| 16 | + convertToNewFormat | ||
| 17 | +} from '@/config/plan-conditions' | ||
| 12 | 18 | ||
| 13 | /** | 19 | /** |
| 14 | * �测循环依赖 | 20 | * �测循环依赖 |
| ... | @@ -88,6 +94,7 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF | ... | @@ -88,6 +94,7 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF |
| 88 | /** | 94 | /** |
| 89 | * 检查字段是否应该显示 | 95 | * 检查字段是否应该显示 |
| 90 | * | 96 | * |
| 97 | + * @description 使用新的条件评估引擎,支持复杂条件(AND/OR/NOT/嵌套) | ||
| 91 | * @param {string} fieldKey - 字段键名 | 98 | * @param {string} fieldKey - 字段键名 |
| 92 | * @returns {boolean} 是否显示 | 99 | * @returns {boolean} 是否显示 |
| 93 | */ | 100 | */ |
| ... | @@ -96,28 +103,16 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF | ... | @@ -96,28 +103,16 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF |
| 96 | const definition = definitions[fieldKey] | 103 | const definition = definitions[fieldKey] |
| 97 | if (!definition) return false | 104 | if (!definition) return false |
| 98 | 105 | ||
| 99 | - // 检查是否有 show_when 条件 | 106 | + // 使用新的条件评估引擎评估 show_when |
| 100 | if (definition.show_when) { | 107 | if (definition.show_when) { |
| 101 | - const conditions = definition.show_when | 108 | + // 转换为新格式(向后兼容) |
| 102 | - if (Array.isArray(conditions)) { | 109 | + const normalizedCondition = convertToNewFormat(definition.show_when) |
| 103 | - for (const condition of conditions) { | 110 | + if (!evaluateCondition(normalizedCondition, formData)) { |
| 104 | - if (!condition) continue | 111 | + return false |
| 105 | - const currentValue = formData[condition.field] | ||
| 106 | - if (currentValue !== condition.equals) { | ||
| 107 | - return false | ||
| 108 | - } | ||
| 109 | - } | ||
| 110 | - } else if (conditions && typeof conditions === 'object') { | ||
| 111 | - for (const [depKey, expectedValue] of Object.entries(conditions)) { | ||
| 112 | - const currentValue = formData[depKey] | ||
| 113 | - if (currentValue !== expectedValue) { | ||
| 114 | - return false | ||
| 115 | - } | ||
| 116 | - } | ||
| 117 | } | 112 | } |
| 118 | } | 113 | } |
| 119 | 114 | ||
| 120 | - // 检查是否被依赖字段影响 | 115 | + // 检查是否被依赖字段影响(旧逻辑,保持兼容) |
| 121 | for (const [key, def] of Object.entries(definitions)) { | 116 | for (const [key, def] of Object.entries(definitions)) { |
| 122 | if (def.affects?.includes(fieldKey)) { | 117 | if (def.affects?.includes(fieldKey)) { |
| 123 | // 依赖字段必须为 true 才显示 | 118 | // 依赖字段必须为 true 才显示 |
| ... | @@ -198,6 +193,88 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF | ... | @@ -198,6 +193,88 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF |
| 198 | // 初始化 | 193 | // 初始化 |
| 199 | initFieldStates() | 194 | initFieldStates() |
| 200 | 195 | ||
| 196 | + /** | ||
| 197 | + * 清理隐藏字段的值 | ||
| 198 | + * | ||
| 199 | + * @description 当字段隐藏时,根据 clear_when_hidden 配置决定是否清空值 | ||
| 200 | + * @param {string} fieldKey - 字段键名 | ||
| 201 | + * @param {Object} definition - 字段定义 | ||
| 202 | + */ | ||
| 203 | + function clearHiddenFieldValue(fieldKey, definition) { | ||
| 204 | + // 默认行为:隐藏时不清空(保持向后兼容) | ||
| 205 | + const clearConfig = definition.clear_when_hidden | ||
| 206 | + | ||
| 207 | + // false 或 undefined:不清空 | ||
| 208 | + if (clearConfig === false || clearConfig === undefined) { | ||
| 209 | + return | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + // true:清空为 undefined | ||
| 213 | + if (clearConfig === true) { | ||
| 214 | + formData[fieldKey] = undefined | ||
| 215 | + return | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + // null:设置为 null | ||
| 219 | + if (clearConfig === null) { | ||
| 220 | + formData[fieldKey] = null | ||
| 221 | + return | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + // 对象配置 | ||
| 225 | + if (typeof clearConfig === 'object') { | ||
| 226 | + // 清空自身 | ||
| 227 | + if (clearConfig.clear_self !== false) { | ||
| 228 | + formData[fieldKey] = undefined | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + // 级联清空依赖字段 | ||
| 232 | + if (Array.isArray(clearConfig.clear_dependents)) { | ||
| 233 | + for (const depKey of clearConfig.clear_dependents) { | ||
| 234 | + if (formData[depKey] !== undefined) { | ||
| 235 | + formData[depKey] = undefined | ||
| 236 | + } | ||
| 237 | + } | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + /** | ||
| 243 | + * 更新字段可见性并处理隐藏字段的清理 | ||
| 244 | + * | ||
| 245 | + * @param {string} fieldKey - 字段键名 | ||
| 246 | + * @param {string} triggerFieldKey - 触发更新的字段键名(可选) | ||
| 247 | + */ | ||
| 248 | + function updateFieldVisibility(fieldKey, triggerFieldKey = null) { | ||
| 249 | + const definitions = getFieldDefinitions() | ||
| 250 | + const definition = definitions[fieldKey] | ||
| 251 | + if (!definition) return | ||
| 252 | + | ||
| 253 | + const wasVisible = fieldVisibility[fieldKey] | ||
| 254 | + const isVisible = isFieldVisible(fieldKey) | ||
| 255 | + fieldVisibility[fieldKey] = isVisible | ||
| 256 | + | ||
| 257 | + // 如果从可见变为不可见,且配置了清理规则,则清理字段值 | ||
| 258 | + if (wasVisible && !isVisible) { | ||
| 259 | + clearHiddenFieldValue(fieldKey, definition) | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + // 更新启用状态 | ||
| 263 | + fieldEnabled[fieldKey] = isFieldEnabled(fieldKey) | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + /** | ||
| 267 | + * 批量更新所有字段的可见性 | ||
| 268 | + * | ||
| 269 | + * @description 通常在表单值变化后调用,重新计算所有字段的可见性 | ||
| 270 | + */ | ||
| 271 | + function refreshAllVisibility() { | ||
| 272 | + const definitions = getFieldDefinitions() | ||
| 273 | + for (const key of Object.keys(definitions)) { | ||
| 274 | + updateFieldVisibility(key) | ||
| 275 | + } | ||
| 276 | + } | ||
| 277 | + | ||
| 201 | return { | 278 | return { |
| 202 | // 状态 | 279 | // 状态 |
| 203 | fieldVisibility, | 280 | fieldVisibility, |
| ... | @@ -208,6 +285,86 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF | ... | @@ -208,6 +285,86 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF |
| 208 | isFieldVisible, | 285 | isFieldVisible, |
| 209 | isFieldEnabled, | 286 | isFieldEnabled, |
| 210 | updateFieldValue, | 287 | updateFieldValue, |
| 211 | - initFieldStates | 288 | + initFieldStates, |
| 289 | + updateFieldVisibility, | ||
| 290 | + refreshAllVisibility | ||
| 291 | + } | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +/** | ||
| 295 | + * 过滤隐藏字段(用于提交前) | ||
| 296 | + * | ||
| 297 | + * @description 过滤掉当前不可见的字段,避免提交脏数据 | ||
| 298 | + * @param {Object} formData - 完整的表单数据 | ||
| 299 | + * @param {string[]} visibleFields - 可见字段列表 | ||
| 300 | + * @param {Object} options - 配置选项 | ||
| 301 | + * @param {string[]} options.alwaysInclude - 始终包含的字段(即使不可见) | ||
| 302 | + * @returns {Object} 过滤后的表单数据 | ||
| 303 | + * | ||
| 304 | + * @example | ||
| 305 | + * const filteredData = filterHiddenFields(formData, visibleFields.value) | ||
| 306 | + * await submitAPI(filteredData) | ||
| 307 | + */ | ||
| 308 | +export function filterHiddenFields(formData, visibleFields, options = {}) { | ||
| 309 | + const { alwaysInclude = [] } = options | ||
| 310 | + const filtered = {} | ||
| 311 | + | ||
| 312 | + for (const [key, value] of Object.entries(formData)) { | ||
| 313 | + // 可见字段始终包含 | ||
| 314 | + if (visibleFields.includes(key)) { | ||
| 315 | + filtered[key] = value | ||
| 316 | + continue | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + // 配置为始终包含的字段 | ||
| 320 | + if (alwaysInclude.includes(key)) { | ||
| 321 | + filtered[key] = value | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + // 其他不可见字段被过滤掉 | ||
| 212 | } | 325 | } |
| 326 | + | ||
| 327 | + return filtered | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +/** | ||
| 331 | + * 获取字段的条件依赖关系(用于调试和可视化) | ||
| 332 | + * | ||
| 333 | + * @description 返回字段的条件依赖图 | ||
| 334 | + * @param {Object} fieldDefinitions - 字段定义 | ||
| 335 | + * @returns {Map<string, Set<string>>} 字段到依赖字段的映射 | ||
| 336 | + */ | ||
| 337 | +export function getFieldDependencyGraph(fieldDefinitions = PLAN_FIELD_DEFINITIONS) { | ||
| 338 | + const graph = new Map() | ||
| 339 | + | ||
| 340 | + for (const [key, definition] of Object.entries(fieldDefinitions)) { | ||
| 341 | + const deps = new Set() | ||
| 342 | + | ||
| 343 | + // 从 show_when 提取依赖 | ||
| 344 | + if (definition.show_when) { | ||
| 345 | + const conditionDeps = getConditionDependencies(definition.show_when) | ||
| 346 | + conditionDeps.forEach(d => deps.add(d)) | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + // 从 affects 提取反向依赖 | ||
| 350 | + if (definition.affects) { | ||
| 351 | + for (const affectedKey of definition.affects) { | ||
| 352 | + if (!graph.has(affectedKey)) { | ||
| 353 | + graph.set(affectedKey, new Set()) | ||
| 354 | + } | ||
| 355 | + graph.get(affectedKey).add(key) | ||
| 356 | + } | ||
| 357 | + } | ||
| 358 | + | ||
| 359 | + // 从 depends_on 提取 | ||
| 360 | + if (definition.depends_on) { | ||
| 361 | + deps.add(definition.depends_on) | ||
| 362 | + } | ||
| 363 | + | ||
| 364 | + if (deps.size > 0) { | ||
| 365 | + graph.set(key, deps) | ||
| 366 | + } | ||
| 367 | + } | ||
| 368 | + | ||
| 369 | + return graph | ||
| 213 | } | 370 | } | ... | ... |
src/config/__tests__/plan-conditions.test.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书字段条件引擎测试 | ||
| 3 | + * | ||
| 4 | + * @description 测试条件操作符和评估逻辑 | ||
| 5 | + * @module config/__tests__/plan-conditions.test | ||
| 6 | + */ | ||
| 7 | + | ||
| 8 | +import { describe, it, expect } from 'vitest' | ||
| 9 | +import { | ||
| 10 | + CONDITION_OPERATORS, | ||
| 11 | + normalizeOperator, | ||
| 12 | + evaluateSingleCondition, | ||
| 13 | + evaluateAnd, | ||
| 14 | + evaluateOr, | ||
| 15 | + evaluateNot, | ||
| 16 | + evaluateCondition, | ||
| 17 | + getConditionDependencies, | ||
| 18 | + convertToNewFormat | ||
| 19 | +} from '../plan-conditions' | ||
| 20 | + | ||
| 21 | +describe('CONDITION_OPERATORS', () => { | ||
| 22 | + describe('比较操作符', () => { | ||
| 23 | + it('eq - 等于', () => { | ||
| 24 | + expect(CONDITION_OPERATORS.eq('是', '是')).toBe(true) | ||
| 25 | + expect(CONDITION_OPERATORS.eq('是', '否')).toBe(false) | ||
| 26 | + expect(CONDITION_OPERATORS.eq(1, 1)).toBe(true) | ||
| 27 | + expect(CONDITION_OPERATORS.eq(1, '1')).toBe(false) // 严格相等 | ||
| 28 | + }) | ||
| 29 | + | ||
| 30 | + it('ne - 不等于', () => { | ||
| 31 | + expect(CONDITION_OPERATORS.ne('是', '否')).toBe(true) | ||
| 32 | + expect(CONDITION_OPERATORS.ne('是', '是')).toBe(false) | ||
| 33 | + }) | ||
| 34 | + | ||
| 35 | + it('gt - 大于', () => { | ||
| 36 | + expect(CONDITION_OPERATORS.gt(30, 18)).toBe(true) | ||
| 37 | + expect(CONDITION_OPERATORS.gt(18, 18)).toBe(false) | ||
| 38 | + expect(CONDITION_OPERATORS.gt(10, 18)).toBe(false) | ||
| 39 | + }) | ||
| 40 | + | ||
| 41 | + it('gte - 大于等于', () => { | ||
| 42 | + expect(CONDITION_OPERATORS.gte(30, 18)).toBe(true) | ||
| 43 | + expect(CONDITION_OPERATORS.gte(18, 18)).toBe(true) | ||
| 44 | + expect(CONDITION_OPERATORS.gte(10, 18)).toBe(false) | ||
| 45 | + }) | ||
| 46 | + | ||
| 47 | + it('lt - 小于', () => { | ||
| 48 | + expect(CONDITION_OPERATORS.lt(10, 18)).toBe(true) | ||
| 49 | + expect(CONDITION_OPERATORS.lt(18, 18)).toBe(false) | ||
| 50 | + expect(CONDITION_OPERATORS.lt(30, 18)).toBe(false) | ||
| 51 | + }) | ||
| 52 | + | ||
| 53 | + it('lte - 小于等于', () => { | ||
| 54 | + expect(CONDITION_OPERATORS.lte(10, 18)).toBe(true) | ||
| 55 | + expect(CONDITION_OPERATORS.lte(18, 18)).toBe(true) | ||
| 56 | + expect(CONDITION_OPERATORS.lte(30, 18)).toBe(false) | ||
| 57 | + }) | ||
| 58 | + }) | ||
| 59 | + | ||
| 60 | + describe('集合操作符', () => { | ||
| 61 | + it('in - 包含于', () => { | ||
| 62 | + expect(CONDITION_OPERATORS.in('A', ['A', 'B', 'C'])).toBe(true) | ||
| 63 | + expect(CONDITION_OPERATORS.in('D', ['A', 'B', 'C'])).toBe(false) | ||
| 64 | + expect(CONDITION_OPERATORS.in('A', 'not-array')).toBe(false) | ||
| 65 | + }) | ||
| 66 | + | ||
| 67 | + it('nin - 不包含于', () => { | ||
| 68 | + expect(CONDITION_OPERATORS.nin('D', ['A', 'B', 'C'])).toBe(true) | ||
| 69 | + expect(CONDITION_OPERATORS.nin('A', ['A', 'B', 'C'])).toBe(false) | ||
| 70 | + }) | ||
| 71 | + }) | ||
| 72 | + | ||
| 73 | + describe('字符串操作符', () => { | ||
| 74 | + it('contains - 包含子串', () => { | ||
| 75 | + expect(CONDITION_OPERATORS.contains('Hello World', 'World')).toBe(true) | ||
| 76 | + expect(CONDITION_OPERATORS.contains('Hello World', 'world')).toBe(false) // 大小写敏感 | ||
| 77 | + expect(CONDITION_OPERATORS.contains(null, 'test')).toBe(false) | ||
| 78 | + }) | ||
| 79 | + | ||
| 80 | + it('startsWith - 前缀匹配', () => { | ||
| 81 | + expect(CONDITION_OPERATORS.startsWith('Hello', 'Hel')).toBe(true) | ||
| 82 | + expect(CONDITION_OPERATORS.startsWith('Hello', 'hel')).toBe(false) | ||
| 83 | + }) | ||
| 84 | + | ||
| 85 | + it('endsWith - 后缀匹配', () => { | ||
| 86 | + expect(CONDITION_OPERATORS.endsWith('Hello', 'llo')).toBe(true) | ||
| 87 | + expect(CONDITION_OPERATORS.endsWith('Hello', 'LLO')).toBe(false) | ||
| 88 | + }) | ||
| 89 | + | ||
| 90 | + it('matches - 正则匹配', () => { | ||
| 91 | + expect(CONDITION_OPERATORS.matches('ABC123', '^[A-Z]+\\d+$')).toBe(true) | ||
| 92 | + expect(CONDITION_OPERATORS.matches('abc', '^[A-Z]+$')).toBe(false) | ||
| 93 | + expect(CONDITION_OPERATORS.matches('test', '[invalid')).toBe(false) // 无效正则 | ||
| 94 | + }) | ||
| 95 | + }) | ||
| 96 | + | ||
| 97 | + describe('布尔/空值操作符', () => { | ||
| 98 | + it('truthy - 真值', () => { | ||
| 99 | + expect(CONDITION_OPERATORS.truthy(true)).toBe(true) | ||
| 100 | + expect(CONDITION_OPERATORS.truthy(1)).toBe(true) | ||
| 101 | + expect(CONDITION_OPERATORS.truthy('text')).toBe(true) | ||
| 102 | + expect(CONDITION_OPERATORS.truthy(false)).toBe(false) | ||
| 103 | + expect(CONDITION_OPERATORS.truthy(0)).toBe(false) | ||
| 104 | + expect(CONDITION_OPERATORS.truthy('')).toBe(false) | ||
| 105 | + expect(CONDITION_OPERATORS.truthy(null)).toBe(false) | ||
| 106 | + }) | ||
| 107 | + | ||
| 108 | + it('falsy - 假值', () => { | ||
| 109 | + expect(CONDITION_OPERATORS.falsy(false)).toBe(true) | ||
| 110 | + expect(CONDITION_OPERATORS.falsy(0)).toBe(true) | ||
| 111 | + expect(CONDITION_OPERATORS.falsy('')).toBe(true) | ||
| 112 | + expect(CONDITION_OPERATORS.falsy(null)).toBe(true) | ||
| 113 | + expect(CONDITION_OPERATORS.falsy(true)).toBe(false) | ||
| 114 | + }) | ||
| 115 | + | ||
| 116 | + it('empty - 空值', () => { | ||
| 117 | + expect(CONDITION_OPERATORS.empty('')).toBe(true) | ||
| 118 | + expect(CONDITION_OPERATORS.empty(null)).toBe(true) | ||
| 119 | + expect(CONDITION_OPERATORS.empty(undefined)).toBe(true) | ||
| 120 | + expect(CONDITION_OPERATORS.empty(0)).toBe(false) | ||
| 121 | + expect(CONDITION_OPERATORS.empty('text')).toBe(false) | ||
| 122 | + }) | ||
| 123 | + | ||
| 124 | + it('notEmpty - 非空', () => { | ||
| 125 | + expect(CONDITION_OPERATORS.notEmpty('text')).toBe(true) | ||
| 126 | + expect(CONDITION_OPERATORS.notEmpty(0)).toBe(true) | ||
| 127 | + expect(CONDITION_OPERATORS.notEmpty('')).toBe(false) | ||
| 128 | + expect(CONDITION_OPERATORS.notEmpty(null)).toBe(false) | ||
| 129 | + }) | ||
| 130 | + | ||
| 131 | + it('emptyArray - 空数组', () => { | ||
| 132 | + expect(CONDITION_OPERATORS.emptyArray([])).toBe(true) | ||
| 133 | + expect(CONDITION_OPERATORS.emptyArray(null)).toBe(true) | ||
| 134 | + expect(CONDITION_OPERATORS.emptyArray([1])).toBe(false) | ||
| 135 | + }) | ||
| 136 | + | ||
| 137 | + it('notEmptyArray - 非空数组', () => { | ||
| 138 | + expect(CONDITION_OPERATORS.notEmptyArray([1])).toBe(true) | ||
| 139 | + expect(CONDITION_OPERATORS.notEmptyArray([])).toBe(false) | ||
| 140 | + }) | ||
| 141 | + }) | ||
| 142 | +}) | ||
| 143 | + | ||
| 144 | +describe('normalizeOperator', () => { | ||
| 145 | + it('返回标准操作符', () => { | ||
| 146 | + expect(normalizeOperator('eq')).toBe('eq') | ||
| 147 | + expect(normalizeOperator('gt')).toBe('gt') | ||
| 148 | + }) | ||
| 149 | + | ||
| 150 | + it('处理别名', () => { | ||
| 151 | + expect(normalizeOperator('equals')).toBe('eq') | ||
| 152 | + expect(normalizeOperator('==')).toBe('eq') | ||
| 153 | + expect(normalizeOperator('===')).toBe('eq') | ||
| 154 | + expect(normalizeOperator('!=')).toBe('ne') | ||
| 155 | + expect(normalizeOperator('>')).toBe('gt') | ||
| 156 | + expect(normalizeOperator('>=')).toBe('gte') | ||
| 157 | + }) | ||
| 158 | + | ||
| 159 | + it('未知操作符返回 null', () => { | ||
| 160 | + expect(normalizeOperator('unknown')).toBe(null) | ||
| 161 | + }) | ||
| 162 | +}) | ||
| 163 | + | ||
| 164 | +describe('evaluateSingleCondition', () => { | ||
| 165 | + const formData = { smoker: '是', age: 30, name: 'John' } | ||
| 166 | + | ||
| 167 | + it('评估等于条件', () => { | ||
| 168 | + expect(evaluateSingleCondition( | ||
| 169 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 170 | + formData | ||
| 171 | + )).toBe(true) | ||
| 172 | + | ||
| 173 | + expect(evaluateSingleCondition( | ||
| 174 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 175 | + formData | ||
| 176 | + )).toBe(false) | ||
| 177 | + }) | ||
| 178 | + | ||
| 179 | + it('评估大于条件', () => { | ||
| 180 | + expect(evaluateSingleCondition( | ||
| 181 | + { field: 'age', op: 'gt', value: 18 }, | ||
| 182 | + formData | ||
| 183 | + )).toBe(true) | ||
| 184 | + }) | ||
| 185 | + | ||
| 186 | + it('评估包含条件', () => { | ||
| 187 | + expect(evaluateSingleCondition( | ||
| 188 | + { field: 'name', op: 'contains', value: 'John' }, | ||
| 189 | + formData | ||
| 190 | + )).toBe(true) | ||
| 191 | + }) | ||
| 192 | + | ||
| 193 | + it('缺少字段或操作符返回 false', () => { | ||
| 194 | + expect(evaluateSingleCondition({ op: 'eq', value: 'x' }, formData)).toBe(false) | ||
| 195 | + expect(evaluateSingleCondition({ field: 'x', value: 'y' }, formData)).toBe(false) | ||
| 196 | + }) | ||
| 197 | + | ||
| 198 | + it('未知操作符返回 false', () => { | ||
| 199 | + expect(evaluateSingleCondition( | ||
| 200 | + { field: 'smoker', op: 'unknown', value: '是' }, | ||
| 201 | + formData | ||
| 202 | + )).toBe(false) | ||
| 203 | + }) | ||
| 204 | +}) | ||
| 205 | + | ||
| 206 | +describe('evaluateAnd', () => { | ||
| 207 | + const formData = { smoker: '是', age: 30 } | ||
| 208 | + | ||
| 209 | + it('所有条件满足返回 true', () => { | ||
| 210 | + expect(evaluateAnd([ | ||
| 211 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 212 | + { field: 'age', op: 'gte', value: 18 } | ||
| 213 | + ], formData)).toBe(true) | ||
| 214 | + }) | ||
| 215 | + | ||
| 216 | + it('任一条件不满足返回 false', () => { | ||
| 217 | + expect(evaluateAnd([ | ||
| 218 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 219 | + { field: 'age', op: 'gte', value: 50 } | ||
| 220 | + ], formData)).toBe(false) | ||
| 221 | + }) | ||
| 222 | + | ||
| 223 | + it('空数组返回 true', () => { | ||
| 224 | + expect(evaluateAnd([], formData)).toBe(true) | ||
| 225 | + }) | ||
| 226 | +}) | ||
| 227 | + | ||
| 228 | +describe('evaluateOr', () => { | ||
| 229 | + const formData = { smoker: '是', age: 30 } | ||
| 230 | + | ||
| 231 | + it('任一条件满足返回 true', () => { | ||
| 232 | + expect(evaluateOr([ | ||
| 233 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 234 | + { field: 'age', op: 'gte', value: 18 } | ||
| 235 | + ], formData)).toBe(true) | ||
| 236 | + }) | ||
| 237 | + | ||
| 238 | + it('所有条件不满足返回 false', () => { | ||
| 239 | + expect(evaluateOr([ | ||
| 240 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 241 | + { field: 'age', op: 'gte', value: 50 } | ||
| 242 | + ], formData)).toBe(false) | ||
| 243 | + }) | ||
| 244 | + | ||
| 245 | + it('空数组返回 false', () => { | ||
| 246 | + expect(evaluateOr([], formData)).toBe(false) | ||
| 247 | + }) | ||
| 248 | +}) | ||
| 249 | + | ||
| 250 | +describe('evaluateNot', () => { | ||
| 251 | + const formData = { smoker: '是' } | ||
| 252 | + | ||
| 253 | + it('条件不满足返回 true', () => { | ||
| 254 | + expect(evaluateNot( | ||
| 255 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 256 | + formData | ||
| 257 | + )).toBe(true) | ||
| 258 | + }) | ||
| 259 | + | ||
| 260 | + it('条件满足返回 false', () => { | ||
| 261 | + expect(evaluateNot( | ||
| 262 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 263 | + formData | ||
| 264 | + )).toBe(false) | ||
| 265 | + }) | ||
| 266 | +}) | ||
| 267 | + | ||
| 268 | +describe('evaluateCondition', () => { | ||
| 269 | + const formData = { | ||
| 270 | + smoker: '是', | ||
| 271 | + age: 30, | ||
| 272 | + product_type: 'A', | ||
| 273 | + coverage: 100000, | ||
| 274 | + payment_period: '20年' | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + it('空条件返回 true', () => { | ||
| 278 | + expect(evaluateCondition(null, formData)).toBe(true) | ||
| 279 | + expect(evaluateCondition(undefined, formData)).toBe(true) | ||
| 280 | + }) | ||
| 281 | + | ||
| 282 | + it('简单条件', () => { | ||
| 283 | + expect(evaluateCondition( | ||
| 284 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 285 | + formData | ||
| 286 | + )).toBe(true) | ||
| 287 | + }) | ||
| 288 | + | ||
| 289 | + it('AND 逻辑', () => { | ||
| 290 | + expect(evaluateCondition({ | ||
| 291 | + and: [ | ||
| 292 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 293 | + { field: 'age', op: 'gte', value: 30 } | ||
| 294 | + ] | ||
| 295 | + }, formData)).toBe(true) | ||
| 296 | + | ||
| 297 | + expect(evaluateCondition({ | ||
| 298 | + and: [ | ||
| 299 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 300 | + { field: 'age', op: 'gt', value: 50 } | ||
| 301 | + ] | ||
| 302 | + }, formData)).toBe(false) | ||
| 303 | + }) | ||
| 304 | + | ||
| 305 | + it('OR 逻辑', () => { | ||
| 306 | + expect(evaluateCondition({ | ||
| 307 | + or: [ | ||
| 308 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 309 | + { field: 'age', op: 'gte', value: 30 } | ||
| 310 | + ] | ||
| 311 | + }, formData)).toBe(true) | ||
| 312 | + | ||
| 313 | + expect(evaluateCondition({ | ||
| 314 | + or: [ | ||
| 315 | + { field: 'smoker', op: 'eq', value: '否' }, | ||
| 316 | + { field: 'age', op: 'gt', value: 50 } | ||
| 317 | + ] | ||
| 318 | + }, formData)).toBe(false) | ||
| 319 | + }) | ||
| 320 | + | ||
| 321 | + it('NOT 逻辑', () => { | ||
| 322 | + expect(evaluateCondition({ | ||
| 323 | + not: { field: 'smoker', op: 'eq', value: '否' } | ||
| 324 | + }, formData)).toBe(true) | ||
| 325 | + }) | ||
| 326 | + | ||
| 327 | + it('嵌套条件', () => { | ||
| 328 | + // (smoker == '是' OR age > 50) AND product_type in ['A', 'B'] | ||
| 329 | + expect(evaluateCondition({ | ||
| 330 | + and: [ | ||
| 331 | + { | ||
| 332 | + or: [ | ||
| 333 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 334 | + { field: 'age', op: 'gt', value: 50 } | ||
| 335 | + ] | ||
| 336 | + }, | ||
| 337 | + { field: 'product_type', op: 'in', value: ['A', 'B'] } | ||
| 338 | + ] | ||
| 339 | + }, formData)).toBe(true) | ||
| 340 | + }) | ||
| 341 | + | ||
| 342 | + it('数组默认为 AND', () => { | ||
| 343 | + expect(evaluateCondition([ | ||
| 344 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 345 | + { field: 'age', op: 'gte', value: 30 } | ||
| 346 | + ], formData)).toBe(true) | ||
| 347 | + }) | ||
| 348 | + | ||
| 349 | + it('旧格式兼容 - equals', () => { | ||
| 350 | + expect(evaluateCondition( | ||
| 351 | + { field: 'smoker', equals: '是' }, | ||
| 352 | + formData | ||
| 353 | + )).toBe(true) | ||
| 354 | + }) | ||
| 355 | + | ||
| 356 | + it('旧格式兼容 - 扁平对象', () => { | ||
| 357 | + expect(evaluateCondition( | ||
| 358 | + { smoker: '是', age: 30 }, | ||
| 359 | + formData | ||
| 360 | + )).toBe(true) | ||
| 361 | + }) | ||
| 362 | +}) | ||
| 363 | + | ||
| 364 | +describe('getConditionDependencies', () => { | ||
| 365 | + it('提取简单条件的依赖', () => { | ||
| 366 | + const deps = getConditionDependencies({ field: 'smoker', op: 'eq', value: '是' }) | ||
| 367 | + expect(deps.has('smoker')).toBe(true) | ||
| 368 | + expect(deps.size).toBe(1) | ||
| 369 | + }) | ||
| 370 | + | ||
| 371 | + it('提取 AND 条件的依赖', () => { | ||
| 372 | + const deps = getConditionDependencies({ | ||
| 373 | + and: [ | ||
| 374 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 375 | + { field: 'age', op: 'gte', value: 30 } | ||
| 376 | + ] | ||
| 377 | + }) | ||
| 378 | + expect(deps.has('smoker')).toBe(true) | ||
| 379 | + expect(deps.has('age')).toBe(true) | ||
| 380 | + expect(deps.size).toBe(2) | ||
| 381 | + }) | ||
| 382 | + | ||
| 383 | + it('提取嵌套条件的依赖', () => { | ||
| 384 | + const deps = getConditionDependencies({ | ||
| 385 | + and: [ | ||
| 386 | + { field: 'a', op: 'eq', value: 1 }, | ||
| 387 | + { | ||
| 388 | + or: [ | ||
| 389 | + { field: 'b', op: 'eq', value: 2 }, | ||
| 390 | + { field: 'c', op: 'eq', value: 3 } | ||
| 391 | + ] | ||
| 392 | + } | ||
| 393 | + ] | ||
| 394 | + }) | ||
| 395 | + expect(deps.has('a')).toBe(true) | ||
| 396 | + expect(deps.has('b')).toBe(true) | ||
| 397 | + expect(deps.has('c')).toBe(true) | ||
| 398 | + expect(deps.size).toBe(3) | ||
| 399 | + }) | ||
| 400 | + | ||
| 401 | + it('提取扁平对象的依赖', () => { | ||
| 402 | + const deps = getConditionDependencies({ smoker: '是', age: 30 }) | ||
| 403 | + expect(deps.has('smoker')).toBe(true) | ||
| 404 | + expect(deps.has('age')).toBe(true) | ||
| 405 | + }) | ||
| 406 | +}) | ||
| 407 | + | ||
| 408 | +describe('convertToNewFormat', () => { | ||
| 409 | + it('已经是新格式则直接返回', () => { | ||
| 410 | + const condition = { field: 'smoker', op: 'eq', value: '是' } | ||
| 411 | + expect(convertToNewFormat(condition)).toEqual(condition) | ||
| 412 | + }) | ||
| 413 | + | ||
| 414 | + it('转换旧格式 equals', () => { | ||
| 415 | + expect(convertToNewFormat( | ||
| 416 | + { field: 'smoker', equals: '是' } | ||
| 417 | + )).toEqual({ field: 'smoker', op: 'eq', value: '是' }) | ||
| 418 | + }) | ||
| 419 | + | ||
| 420 | + it('转换旧格式数组为 AND', () => { | ||
| 421 | + expect(convertToNewFormat([ | ||
| 422 | + { field: 'a', equals: 'x' }, | ||
| 423 | + { field: 'b', equals: 'y' } | ||
| 424 | + ])).toEqual({ | ||
| 425 | + and: [ | ||
| 426 | + { field: 'a', op: 'eq', value: 'x' }, | ||
| 427 | + { field: 'b', op: 'eq', value: 'y' } | ||
| 428 | + ] | ||
| 429 | + }) | ||
| 430 | + }) | ||
| 431 | + | ||
| 432 | + it('单个条件的数组不包装 AND', () => { | ||
| 433 | + expect(convertToNewFormat([ | ||
| 434 | + { field: 'a', equals: 'x' } | ||
| 435 | + ])).toEqual({ field: 'a', op: 'eq', value: 'x' }) | ||
| 436 | + }) | ||
| 437 | + | ||
| 438 | + it('转换扁平对象', () => { | ||
| 439 | + expect(convertToNewFormat({ smoker: '是' })).toEqual({ | ||
| 440 | + field: 'smoker', | ||
| 441 | + op: 'eq', | ||
| 442 | + value: '是' | ||
| 443 | + }) | ||
| 444 | + }) | ||
| 445 | + | ||
| 446 | + it('转换多字段扁平对象为 AND', () => { | ||
| 447 | + expect(convertToNewFormat({ smoker: '是', age: 30 })).toEqual({ | ||
| 448 | + and: [ | ||
| 449 | + { field: 'smoker', op: 'eq', value: '是' }, | ||
| 450 | + { field: 'age', op: 'eq', value: 30 } | ||
| 451 | + ] | ||
| 452 | + }) | ||
| 453 | + }) | ||
| 454 | +}) |
src/config/plan-conditions.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书字段条件规则引擎 | ||
| 3 | + * | ||
| 4 | + * @description 定义条件操作符和评估逻辑,支持复杂的字段显示/隐藏条件 | ||
| 5 | + * @module config/plan-conditions | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-15 | ||
| 8 | + * @version 1.0.0 | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 条件操作符定义 | ||
| 13 | + * | ||
| 14 | + * @description 所有可用的条件比较操作符 | ||
| 15 | + * @type {Object<string, Function>} | ||
| 16 | + */ | ||
| 17 | +export const CONDITION_OPERATORS = { | ||
| 18 | + // ========== 比较操作 ========== | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 等于(严格相等) | ||
| 22 | + * @param {*} actual - 实际值 | ||
| 23 | + * @param {*} expected - 期望值 | ||
| 24 | + * @returns {boolean} | ||
| 25 | + */ | ||
| 26 | + eq: (actual, expected) => actual === expected, | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 不等于 | ||
| 30 | + * @param {*} actual - 实际值 | ||
| 31 | + * @param {*} expected - 期望值 | ||
| 32 | + * @returns {boolean} | ||
| 33 | + */ | ||
| 34 | + ne: (actual, expected) => actual !== expected, | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 大于 | ||
| 38 | + * @param {number} actual - 实际值 | ||
| 39 | + * @param {number} expected - 期望值 | ||
| 40 | + * @returns {boolean} | ||
| 41 | + */ | ||
| 42 | + gt: (actual, expected) => { | ||
| 43 | + const num = Number(actual) | ||
| 44 | + const exp = Number(expected) | ||
| 45 | + return !Number.isNaN(num) && !Number.isNaN(exp) && num > exp | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 大于等于 | ||
| 50 | + * @param {number} actual - 实际值 | ||
| 51 | + * @param {number} expected - 期望值 | ||
| 52 | + * @returns {boolean} | ||
| 53 | + */ | ||
| 54 | + gte: (actual, expected) => { | ||
| 55 | + const num = Number(actual) | ||
| 56 | + const exp = Number(expected) | ||
| 57 | + return !Number.isNaN(num) && !Number.isNaN(exp) && num >= exp | ||
| 58 | + }, | ||
| 59 | + | ||
| 60 | + /** | ||
| 61 | + * 小于 | ||
| 62 | + * @param {number} actual - 实际值 | ||
| 63 | + * @param {number} expected - 期望值 | ||
| 64 | + * @returns {boolean} | ||
| 65 | + */ | ||
| 66 | + lt: (actual, expected) => { | ||
| 67 | + const num = Number(actual) | ||
| 68 | + const exp = Number(expected) | ||
| 69 | + return !Number.isNaN(num) && !Number.isNaN(exp) && num < exp | ||
| 70 | + }, | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 小于等于 | ||
| 74 | + * @param {number} actual - 实际值 | ||
| 75 | + * @param {number} expected - 期望值 | ||
| 76 | + * @returns {boolean} | ||
| 77 | + */ | ||
| 78 | + lte: (actual, expected) => { | ||
| 79 | + const num = Number(actual) | ||
| 80 | + const exp = Number(expected) | ||
| 81 | + return !Number.isNaN(num) && !Number.isNaN(exp) && num <= exp | ||
| 82 | + }, | ||
| 83 | + | ||
| 84 | + // ========== 集合操作 ========== | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 包含于(值在数组中) | ||
| 88 | + * @param {*} actual - 实际值 | ||
| 89 | + * @param {Array} expected - 期望数组 | ||
| 90 | + * @returns {boolean} | ||
| 91 | + */ | ||
| 92 | + in: (actual, expected) => { | ||
| 93 | + if (!Array.isArray(expected)) return false | ||
| 94 | + return expected.includes(actual) | ||
| 95 | + }, | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 不包含于(值不在数组中) | ||
| 99 | + * @param {*} actual - 实际值 | ||
| 100 | + * @param {Array} expected - 期望数组 | ||
| 101 | + * @returns {boolean} | ||
| 102 | + */ | ||
| 103 | + nin: (actual, expected) => { | ||
| 104 | + if (!Array.isArray(expected)) return true | ||
| 105 | + return !expected.includes(actual) | ||
| 106 | + }, | ||
| 107 | + | ||
| 108 | + // ========== 字符串操作 ========== | ||
| 109 | + | ||
| 110 | + /** | ||
| 111 | + * 字符串包含 | ||
| 112 | + * @param {string} actual - 实际值 | ||
| 113 | + * @param {string} expected - 期望子串 | ||
| 114 | + * @returns {boolean} | ||
| 115 | + */ | ||
| 116 | + contains: (actual, expected) => { | ||
| 117 | + return String(actual ?? '').includes(String(expected ?? '')) | ||
| 118 | + }, | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 字符串前缀匹配 | ||
| 122 | + * @param {string} actual - 实际值 | ||
| 123 | + * @param {string} expected - 期望前缀 | ||
| 124 | + * @returns {boolean} | ||
| 125 | + */ | ||
| 126 | + startsWith: (actual, expected) => { | ||
| 127 | + return String(actual ?? '').startsWith(String(expected ?? '')) | ||
| 128 | + }, | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 字符串后缀匹配 | ||
| 132 | + * @param {string} actual - 实际值 | ||
| 133 | + * @param {string} expected - 期望后缀 | ||
| 134 | + * @returns {boolean} | ||
| 135 | + */ | ||
| 136 | + endsWith: (actual, expected) => { | ||
| 137 | + return String(actual ?? '').endsWith(String(expected ?? '')) | ||
| 138 | + }, | ||
| 139 | + | ||
| 140 | + /** | ||
| 141 | + * 正则表达式匹配 | ||
| 142 | + * @param {string} actual - 实际值 | ||
| 143 | + * @param {string|RegExp} expected - 正则表达式 | ||
| 144 | + * @returns {boolean} | ||
| 145 | + */ | ||
| 146 | + matches: (actual, expected) => { | ||
| 147 | + try { | ||
| 148 | + const regex = expected instanceof RegExp ? expected : new RegExp(expected) | ||
| 149 | + return regex.test(String(actual ?? '')) | ||
| 150 | + } catch { | ||
| 151 | + return false | ||
| 152 | + } | ||
| 153 | + }, | ||
| 154 | + | ||
| 155 | + // ========== 布尔/空值操作 ========== | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 真值检查 | ||
| 159 | + * @param {*} actual - 实际值 | ||
| 160 | + * @returns {boolean} | ||
| 161 | + */ | ||
| 162 | + truthy: (actual) => !!actual, | ||
| 163 | + | ||
| 164 | + /** | ||
| 165 | + * 假值检查 | ||
| 166 | + * @param {*} actual - 实际值 | ||
| 167 | + * @returns {boolean} | ||
| 168 | + */ | ||
| 169 | + falsy: (actual) => !actual, | ||
| 170 | + | ||
| 171 | + /** | ||
| 172 | + * 空值检查(null, undefined, 空字符串) | ||
| 173 | + * @param {*} actual - 实际值 | ||
| 174 | + * @returns {boolean} | ||
| 175 | + */ | ||
| 176 | + empty: (actual) => actual === '' || actual === null || actual === undefined, | ||
| 177 | + | ||
| 178 | + /** | ||
| 179 | + * 非空检查 | ||
| 180 | + * @param {*} actual - 实际值 | ||
| 181 | + * @returns {boolean} | ||
| 182 | + */ | ||
| 183 | + notEmpty: (actual) => actual !== '' && actual !== null && actual !== undefined, | ||
| 184 | + | ||
| 185 | + /** | ||
| 186 | + * 空数组检查 | ||
| 187 | + * @param {*} actual - 实际值 | ||
| 188 | + * @returns {boolean} | ||
| 189 | + */ | ||
| 190 | + emptyArray: (actual) => !Array.isArray(actual) || actual.length === 0, | ||
| 191 | + | ||
| 192 | + /** | ||
| 193 | + * 非空数组检查 | ||
| 194 | + * @param {*} actual - 实际值 | ||
| 195 | + * @returns {boolean} | ||
| 196 | + */ | ||
| 197 | + notEmptyArray: (actual) => Array.isArray(actual) && actual.length > 0 | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +/** | ||
| 201 | + * 操作符别名映射(向后兼容) | ||
| 202 | + * | ||
| 203 | + * @description 旧格式操作符到新格式的映射 | ||
| 204 | + * @type {Object<string, string>} | ||
| 205 | + */ | ||
| 206 | +export const OPERATOR_ALIASES = { | ||
| 207 | + equals: 'eq', | ||
| 208 | + '==': 'eq', | ||
| 209 | + '===': 'eq', | ||
| 210 | + '!=': 'ne', | ||
| 211 | + '!==': 'ne', | ||
| 212 | + '>': 'gt', | ||
| 213 | + '>=': 'gte', | ||
| 214 | + '<': 'lt', | ||
| 215 | + '<=': 'lte' | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +/** | ||
| 219 | + * 解析操作符(支持别名) | ||
| 220 | + * | ||
| 221 | + * @param {string} op - 操作符名称或别名 | ||
| 222 | + * @returns {string|null} 标准操作符名称 | ||
| 223 | + */ | ||
| 224 | +export function normalizeOperator(op) { | ||
| 225 | + if (CONDITION_OPERATORS[op]) return op | ||
| 226 | + if (OPERATOR_ALIASES[op]) return OPERATOR_ALIASES[op] | ||
| 227 | + return null | ||
| 228 | +} | ||
| 229 | + | ||
| 230 | +/** | ||
| 231 | + * 评估单个条件 | ||
| 232 | + * | ||
| 233 | + * @description 评估一个简单的条件表达式 | ||
| 234 | + * @param {Object} condition - 条件对象 | ||
| 235 | + * @param {string} condition.field - 字段名 | ||
| 236 | + * @param {string} condition.op - 操作符 | ||
| 237 | + * @param {*} condition.value - 期望值 | ||
| 238 | + * @param {Object} formData - 表单数据 | ||
| 239 | + * @returns {boolean} 条件是否满足 | ||
| 240 | + * | ||
| 241 | + * @example | ||
| 242 | + * evaluateSingleCondition( | ||
| 243 | + * { field: 'smoker', op: 'eq', value: '是' }, | ||
| 244 | + * { smoker: '是', age: 30 } | ||
| 245 | + * ) // true | ||
| 246 | + */ | ||
| 247 | +export function evaluateSingleCondition(condition, formData) { | ||
| 248 | + const { field, op, value } = condition | ||
| 249 | + | ||
| 250 | + if (!field || !op) { | ||
| 251 | + console.warn('[plan-conditions] 条件缺少 field 或 op:', condition) | ||
| 252 | + return false | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + const normalizedOp = normalizeOperator(op) | ||
| 256 | + if (!normalizedOp) { | ||
| 257 | + console.warn(`[plan-conditions] 未知操作符: ${op}`) | ||
| 258 | + return false | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + const operator = CONDITION_OPERATORS[normalizedOp] | ||
| 262 | + const actualValue = formData[field] | ||
| 263 | + | ||
| 264 | + try { | ||
| 265 | + return operator(actualValue, value) | ||
| 266 | + } catch (err) { | ||
| 267 | + console.error(`[plan-conditions] 条件评估失败:`, err) | ||
| 268 | + return false | ||
| 269 | + } | ||
| 270 | +} | ||
| 271 | + | ||
| 272 | +/** | ||
| 273 | + * 评估 AND 逻辑 | ||
| 274 | + * | ||
| 275 | + * @param {Array} conditions - 条件数组 | ||
| 276 | + * @param {Object} formData - 表单数据 | ||
| 277 | + * @returns {boolean} 所有条件是否都满足 | ||
| 278 | + */ | ||
| 279 | +export function evaluateAnd(conditions, formData) { | ||
| 280 | + if (!Array.isArray(conditions) || conditions.length === 0) return true | ||
| 281 | + return conditions.every(c => evaluateCondition(c, formData)) | ||
| 282 | +} | ||
| 283 | + | ||
| 284 | +/** | ||
| 285 | + * 评估 OR 逻辑 | ||
| 286 | + * | ||
| 287 | + * @param {Array} conditions - 条件数组 | ||
| 288 | + * @param {Object} formData - 表单数据 | ||
| 289 | + * @returns {boolean} 是否有任一条件满足 | ||
| 290 | + */ | ||
| 291 | +export function evaluateOr(conditions, formData) { | ||
| 292 | + if (!Array.isArray(conditions) || conditions.length === 0) return false | ||
| 293 | + return conditions.some(c => evaluateCondition(c, formData)) | ||
| 294 | +} | ||
| 295 | + | ||
| 296 | +/** | ||
| 297 | + * 评估 NOT 逻辑 | ||
| 298 | + * | ||
| 299 | + * @param {Object} condition - 条件对象 | ||
| 300 | + * @param {Object} formData - 表单数据 | ||
| 301 | + * @returns {boolean} 条件是否不满足 | ||
| 302 | + */ | ||
| 303 | +export function evaluateNot(condition, formData) { | ||
| 304 | + if (!condition) return false | ||
| 305 | + return !evaluateCondition(condition, formData) | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +/** | ||
| 309 | + * 评估条件(主入口) | ||
| 310 | + * | ||
| 311 | + * @description 评估任意条件表达式,支持简单条件和逻辑组合 | ||
| 312 | + * @param {Object|Array} condition - 条件表达式 | ||
| 313 | + * @param {Object} formData - 表单数据 | ||
| 314 | + * @returns {boolean} 条件是否满足 | ||
| 315 | + * | ||
| 316 | + * @example | ||
| 317 | + * // 简单条件 | ||
| 318 | + * evaluateCondition({ field: 'smoker', op: 'eq', value: '是' }, formData) | ||
| 319 | + * | ||
| 320 | + * @example | ||
| 321 | + * // AND 条件 | ||
| 322 | + * evaluateCondition({ | ||
| 323 | + * and: [ | ||
| 324 | + * { field: 'smoker', op: 'eq', value: '是' }, | ||
| 325 | + * { field: 'age', op: 'gte', value: 30 } | ||
| 326 | + * ] | ||
| 327 | + * }, formData) | ||
| 328 | + * | ||
| 329 | + * @example | ||
| 330 | + * // OR 条件 | ||
| 331 | + * evaluateCondition({ | ||
| 332 | + * or: [ | ||
| 333 | + * { field: 'smoker', op: 'eq', value: '是' }, | ||
| 334 | + * { field: 'age', op: 'gt', value: 50 } | ||
| 335 | + * ] | ||
| 336 | + * }, formData) | ||
| 337 | + * | ||
| 338 | + * @example | ||
| 339 | + * // 嵌套条件 | ||
| 340 | + * evaluateCondition({ | ||
| 341 | + * and: [ | ||
| 342 | + * { field: 'type', op: 'in', value: ['A', 'B'] }, | ||
| 343 | + * { | ||
| 344 | + * or: [ | ||
| 345 | + * { field: 'coverage', op: 'gte', value: 100000 }, | ||
| 346 | + * { field: 'period', op: 'eq', value: '20年' } | ||
| 347 | + * ] | ||
| 348 | + * } | ||
| 349 | + * ] | ||
| 350 | + * }, formData) | ||
| 351 | + */ | ||
| 352 | +export function evaluateCondition(condition, formData) { | ||
| 353 | + // 空条件默认为 true | ||
| 354 | + if (!condition) return true | ||
| 355 | + | ||
| 356 | + // 数组条件:默认为 AND 逻辑 | ||
| 357 | + if (Array.isArray(condition)) { | ||
| 358 | + return evaluateAnd(condition, formData) | ||
| 359 | + } | ||
| 360 | + | ||
| 361 | + // 对象条件 | ||
| 362 | + if (typeof condition === 'object') { | ||
| 363 | + // AND 逻辑 | ||
| 364 | + if (condition.and) { | ||
| 365 | + return evaluateAnd(condition.and, formData) | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + // OR 逻辑 | ||
| 369 | + if (condition.or) { | ||
| 370 | + return evaluateOr(condition.or, formData) | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + // NOT 逻辑 | ||
| 374 | + if (condition.not) { | ||
| 375 | + return evaluateNot(condition.not, formData) | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + // 简单条件(包含 field 和 op) | ||
| 379 | + if (condition.field && condition.op) { | ||
| 380 | + return evaluateSingleCondition(condition, formData) | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + // 旧格式兼容:{ field: 'xxx', equals: 'yyy' } | ||
| 384 | + if (condition.field && condition.equals !== undefined) { | ||
| 385 | + return evaluateSingleCondition( | ||
| 386 | + { field: condition.field, op: 'eq', value: condition.equals }, | ||
| 387 | + formData | ||
| 388 | + ) | ||
| 389 | + } | ||
| 390 | + | ||
| 391 | + // 旧格式兼容:{ field: 'xxx', not_equals: 'yyy' } | ||
| 392 | + if (condition.field && condition.not_equals !== undefined) { | ||
| 393 | + return evaluateSingleCondition( | ||
| 394 | + { field: condition.field, op: 'ne', value: condition.not_equals }, | ||
| 395 | + formData | ||
| 396 | + ) | ||
| 397 | + } | ||
| 398 | + | ||
| 399 | + // 扁平条件对象:{ smoker: '是', age: 30 } → 所有条件 AND | ||
| 400 | + const keys = Object.keys(condition) | ||
| 401 | + if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op'].includes(k))) { | ||
| 402 | + return keys.every(field => { | ||
| 403 | + const expectedValue = condition[field] | ||
| 404 | + const actualValue = formData[field] | ||
| 405 | + return actualValue === expectedValue | ||
| 406 | + }) | ||
| 407 | + } | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + console.warn('[plan-conditions] 无法识别的条件格式:', condition) | ||
| 411 | + return false | ||
| 412 | +} | ||
| 413 | + | ||
| 414 | +/** | ||
| 415 | + * 获取条件依赖的字段列表 | ||
| 416 | + * | ||
| 417 | + * @description 从条件表达式中提取所有依赖的字段名 | ||
| 418 | + * @param {Object|Array} condition - 条件表达式 | ||
| 419 | + * @returns {Set<string>} 依赖的字段名集合 | ||
| 420 | + * | ||
| 421 | + * @example | ||
| 422 | + * getConditionDependencies({ and: [{ field: 'a', op: 'eq', value: 1 }, { field: 'b', op: 'eq', value: 2 }] }) | ||
| 423 | + * // Set { 'a', 'b' } | ||
| 424 | + */ | ||
| 425 | +export function getConditionDependencies(condition) { | ||
| 426 | + const deps = new Set() | ||
| 427 | + | ||
| 428 | + if (!condition) return deps | ||
| 429 | + | ||
| 430 | + // 数组条件 | ||
| 431 | + if (Array.isArray(condition)) { | ||
| 432 | + condition.forEach(c => { | ||
| 433 | + const subDeps = getConditionDependencies(c) | ||
| 434 | + subDeps.forEach(d => deps.add(d)) | ||
| 435 | + }) | ||
| 436 | + return deps | ||
| 437 | + } | ||
| 438 | + | ||
| 439 | + if (typeof condition === 'object') { | ||
| 440 | + // 简单条件 | ||
| 441 | + if (condition.field) { | ||
| 442 | + deps.add(condition.field) | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + // 扁平条件对象 | ||
| 446 | + const keys = Object.keys(condition) | ||
| 447 | + if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op', 'equals'].includes(k))) { | ||
| 448 | + keys.forEach(k => deps.add(k)) | ||
| 449 | + } | ||
| 450 | + | ||
| 451 | + // 递归处理逻辑组合 | ||
| 452 | + if (condition.and) { | ||
| 453 | + condition.and.forEach(c => { | ||
| 454 | + const subDeps = getConditionDependencies(c) | ||
| 455 | + subDeps.forEach(d => deps.add(d)) | ||
| 456 | + }) | ||
| 457 | + } | ||
| 458 | + if (condition.or) { | ||
| 459 | + condition.or.forEach(c => { | ||
| 460 | + const subDeps = getConditionDependencies(c) | ||
| 461 | + subDeps.forEach(d => deps.add(d)) | ||
| 462 | + }) | ||
| 463 | + } | ||
| 464 | + if (condition.not) { | ||
| 465 | + const subDeps = getConditionDependencies(condition.not) | ||
| 466 | + subDeps.forEach(d => deps.add(d)) | ||
| 467 | + } | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + return deps | ||
| 471 | +} | ||
| 472 | + | ||
| 473 | +/** | ||
| 474 | + * 将旧格式转换为新格式 | ||
| 475 | + * | ||
| 476 | + * @description 将旧的 show_when 格式转换为新的条件格式 | ||
| 477 | + * @param {Object|Array|string} oldFormat - 旧格式条件 | ||
| 478 | + * @returns {Object|null} 新格式条件 | ||
| 479 | + * | ||
| 480 | + * @example | ||
| 481 | + * // 旧格式 | ||
| 482 | + * { field: 'withdrawal_mode', equals: '指定提取金额' } | ||
| 483 | + * // 转换为 | ||
| 484 | + * { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' } | ||
| 485 | + * | ||
| 486 | + * @example | ||
| 487 | + * // 旧格式数组 | ||
| 488 | + * [{ field: 'a', equals: 'x' }, { field: 'b', equals: 'y' }] | ||
| 489 | + * // 转换为 | ||
| 490 | + * { and: [{ field: 'a', op: 'eq', value: 'x' }, { field: 'b', op: 'eq', value: 'y' }] } | ||
| 491 | + */ | ||
| 492 | +export function convertToNewFormat(oldFormat) { | ||
| 493 | + if (!oldFormat) return null | ||
| 494 | + | ||
| 495 | + // 已经是新格式 | ||
| 496 | + if (oldFormat.and || oldFormat.or || oldFormat.not || oldFormat.op) { | ||
| 497 | + return oldFormat | ||
| 498 | + } | ||
| 499 | + | ||
| 500 | + // 旧格式单个条件 | ||
| 501 | + if (oldFormat.field && oldFormat.equals !== undefined) { | ||
| 502 | + return { | ||
| 503 | + field: oldFormat.field, | ||
| 504 | + op: 'eq', | ||
| 505 | + value: oldFormat.equals | ||
| 506 | + } | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + // 旧格式数组 | ||
| 510 | + if (Array.isArray(oldFormat) && oldFormat.length > 0) { | ||
| 511 | + const conditions = oldFormat.map(c => { | ||
| 512 | + if (c.field && c.equals !== undefined) { | ||
| 513 | + return { field: c.field, op: 'eq', value: c.equals } | ||
| 514 | + } | ||
| 515 | + return c | ||
| 516 | + }) | ||
| 517 | + | ||
| 518 | + // 单个条件不需要包装 and | ||
| 519 | + if (conditions.length === 1) { | ||
| 520 | + return conditions[0] | ||
| 521 | + } | ||
| 522 | + | ||
| 523 | + return { and: conditions } | ||
| 524 | + } | ||
| 525 | + | ||
| 526 | + // 扁平对象格式 { smoker: '是' } | ||
| 527 | + const keys = Object.keys(oldFormat) | ||
| 528 | + if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op', 'equals'].includes(k))) { | ||
| 529 | + if (keys.length === 1) { | ||
| 530 | + return { field: keys[0], op: 'eq', value: oldFormat[keys[0]] } | ||
| 531 | + } | ||
| 532 | + return { | ||
| 533 | + and: keys.map(field => ({ field, op: 'eq', value: oldFormat[field] })) | ||
| 534 | + } | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + return oldFormat | ||
| 538 | +} |
| ... | @@ -72,6 +72,7 @@ const savingsSubmitMapping = { | ... | @@ -72,6 +72,7 @@ const savingsSubmitMapping = { |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | // 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | 74 | // 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) |
| 75 | +// @updated 2026-02-15 - 迁移到新的条件规则格式,使用 clear_when_hidden 替代 reset_map | ||
| 75 | const savingsFormSchema = { | 76 | const savingsFormSchema = { |
| 76 | // 基础字段:非提取计划部分 | 77 | // 基础字段:非提取计划部分 |
| 77 | base_fields: [ | 78 | base_fields: [ |
| ... | @@ -83,24 +84,25 @@ const savingsFormSchema = { | ... | @@ -83,24 +84,25 @@ const savingsFormSchema = { |
| 83 | { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | 84 | { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } |
| 84 | ], | 85 | ], |
| 85 | // 提取计划字段:由 withdrawal_plan 开关控制 | 86 | // 提取计划字段:由 withdrawal_plan 开关控制 |
| 87 | + // 新格式说明: | ||
| 88 | + // - show_when: { field: 'xxx', op: 'eq', value: 'yyy' } 新格式条件 | ||
| 89 | + // - show_when: { field: 'xxx', equals: 'yyy' } 旧格式(向后兼容) | ||
| 90 | + // - clear_when_hidden: true 隐藏时自动清空字段值 | ||
| 86 | withdrawal_fields: [ | 91 | withdrawal_fields: [ |
| 87 | { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, | 92 | { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' }, |
| 88 | - { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' }, | 93 | + { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)', clear_when_hidden: true }, |
| 89 | - { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | 94 | + // 指定提取金额模式字段 |
| 90 | - { 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: '指定提取金额' }] }, | 95 | + { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true }, |
| 91 | - { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | 96 | + { 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', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true }, |
| 92 | - { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | 97 | + { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true }, |
| 93 | - { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] }, | 98 | + { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true }, |
| 94 | - { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }, | 99 | + { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true }, |
| 95 | - { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] } | 100 | + // 最高固定提取金额模式字段 |
| 96 | - ], | 101 | + { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '最高固定提取金额' }, clear_when_hidden: true }, |
| 97 | - // 提取模式切换时的清空逻辑,避免脏字段影响提交 | 102 | + { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: { field: 'withdrawal_mode', op: 'eq', value: '最高固定提取金额' }, clear_when_hidden: true } |
| 98 | - reset_map: { | 103 | + ] |
| 99 | - withdrawal_mode: { | 104 | + // reset_map 已被 clear_when_hidden 替代 |
| 100 | - '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'], | 105 | + // 当 withdrawal_mode 切换时,不可见的字段会自动清空 |
| 101 | - '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed'] | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | } | 106 | } |
| 105 | 107 | ||
| 106 | export const PLAN_TEMPLATES = { | 108 | export const PLAN_TEMPLATES = { |
| ... | @@ -366,6 +368,24 @@ export const PLAN_TEMPLATES = { | ... | @@ -366,6 +368,24 @@ export const PLAN_TEMPLATES = { |
| 366 | submit_mapping: savingsSubmitMapping | 368 | submit_mapping: savingsSubmitMapping |
| 367 | } | 369 | } |
| 368 | }, | 370 | }, |
| 371 | + | ||
| 372 | + /** | ||
| 373 | + * 长宁終身壽險計劃3 | ||
| 374 | + * @added 2026-02-15T06:30:03.691Z | ||
| 375 | + * @source docs/to-parse/计划书模版4.docx | ||
| 376 | + */ | ||
| 377 | + 'life-insurance-3-d8fde07d': { | ||
| 378 | + name: '长宁終身壽險計劃3', | ||
| 379 | + component: 'LifeInsuranceTemplate', | ||
| 380 | + config: { | ||
| 381 | + currency: 'USD', | ||
| 382 | + payment_periods: ["5年","12年","15年","20年"], | ||
| 383 | + age_range: { min: 0, max: 75 }, | ||
| 384 | + insurance_period: '终身', | ||
| 385 | + form_schema: protectionFormSchema, | ||
| 386 | + submit_mapping: baseSubmitMapping | ||
| 387 | + } | ||
| 388 | + } | ||
| 369 | } | 389 | } |
| 370 | 390 | ||
| 371 | /** | 391 | /** | ... | ... |
| ... | @@ -415,9 +415,11 @@ export async function mockProductListAPI(params) { | ... | @@ -415,9 +415,11 @@ export async function mockProductListAPI(params) { |
| 415 | const list = [] | 415 | const list = [] |
| 416 | const startIndex = page * limit | 416 | const startIndex = page * limit |
| 417 | 417 | ||
| 418 | - // 🔧 测试商品:第一页第一位固定为储蓄产品(form_sn:savings-product-30b41aae) | 418 | + // 🔧 测试商品:第一页前两位固定为测试产品 |
| 419 | if (page === 0) { | 419 | if (page === 0) { |
| 420 | const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1) | 420 | const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1) |
| 421 | + | ||
| 422 | + // 测试商品1: 储蓄产品 | ||
| 421 | const testProduct1 = { | 423 | const testProduct1 = { |
| 422 | id: 'savings-2-148b3acd', | 424 | id: 'savings-2-148b3acd', |
| 423 | product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)', | 425 | product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)', |
| ... | @@ -432,19 +434,38 @@ export async function mockProductListAPI(params) { | ... | @@ -432,19 +434,38 @@ export async function mockProductListAPI(params) { |
| 432 | _test_note: 'form_sn:savings-2-148b3acd' | 434 | _test_note: 'form_sn:savings-2-148b3acd' |
| 433 | } | 435 | } |
| 434 | 436 | ||
| 435 | - // 检查分类和关键词过滤 | 437 | + // 测试商品2: 人寿保险产品 |
| 436 | - let shouldInclude = true | 438 | + const testProduct2 = { |
| 437 | - if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) { | 439 | + id: 'life-insurance-3-d8fde07d', |
| 438 | - shouldInclude = false | 440 | + product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)', |
| 439 | - } | 441 | + cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300', |
| 440 | - if (keyword && !testProduct1.product_name.includes(keyword)) { | 442 | + recommend: 'hot', |
| 441 | - shouldInclude = false | 443 | + form_sn: 'life-insurance-3-d8fde07d', // ✅ 关键字段:对应真实 API 的 form_sn |
| 444 | + created_time: new Date().toISOString(), | ||
| 445 | + categories: [testCategory], // ✅ 符合真实 API 结构:categories 是数组 | ||
| 446 | + tags: [{ id: '1', name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }], | ||
| 447 | + // 测试标识(不影响业务逻辑) | ||
| 448 | + _test: true, | ||
| 449 | + _test_note: 'form_sn:life-insurance-3-d8fde07d' | ||
| 442 | } | 450 | } |
| 443 | 451 | ||
| 444 | - if (shouldInclude) { | 452 | + // 检查分类和关键词过滤,依次添加测试商品 |
| 445 | - list.push(testProduct1) | 453 | + const testProducts = [testProduct1, testProduct2] |
| 446 | - console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd') | 454 | + |
| 447 | - } | 455 | + testProducts.forEach((testProduct, index) => { |
| 456 | + let shouldInclude = true | ||
| 457 | + if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) { | ||
| 458 | + shouldInclude = false | ||
| 459 | + } | ||
| 460 | + if (keyword && !testProduct.product_name.includes(keyword)) { | ||
| 461 | + shouldInclude = false | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + if (shouldInclude) { | ||
| 465 | + list.push(testProduct) | ||
| 466 | + console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`) | ||
| 467 | + } | ||
| 468 | + }) | ||
| 448 | } | 469 | } |
| 449 | 470 | ||
| 450 | for (let i = 0; i < limit; i++) { | 471 | for (let i = 0; i < limit; i++) { | ... | ... |
| 1 | + | ... | ... |
| ... | @@ -234,6 +234,16 @@ function resolveSchemaRefs(config) { | ... | @@ -234,6 +234,16 @@ function resolveSchemaRefs(config) { |
| 234 | } | 234 | } |
| 235 | } | 235 | } |
| 236 | 236 | ||
| 237 | +/** | ||
| 238 | + * 构建表单 Schema 代码 | ||
| 239 | + * | ||
| 240 | + * @description 将 schema 对象转换为代码字符串 | ||
| 241 | + * @param {Object|string} value - schema 值 | ||
| 242 | + * @param {string} fallbackRef - 回退引用名 | ||
| 243 | + * @returns {string} 代码字符串 | ||
| 244 | + * | ||
| 245 | + * @updated 2026-02-15 - 支持新的条件格式 show_when: { field, op, value } | ||
| 246 | + */ | ||
| 237 | function buildSchemaCode(value, fallbackRef) { | 247 | function buildSchemaCode(value, fallbackRef) { |
| 238 | if (!value || isEmptyObject(value)) { | 248 | if (!value || isEmptyObject(value)) { |
| 239 | return fallbackRef | 249 | return fallbackRef |
| ... | @@ -241,13 +251,14 @@ function buildSchemaCode(value, fallbackRef) { | ... | @@ -241,13 +251,14 @@ function buildSchemaCode(value, fallbackRef) { |
| 241 | if (value && typeof value === 'object' && !Array.isArray(value)) { | 251 | if (value && typeof value === 'object' && !Array.isArray(value)) { |
| 242 | const baseFields = value.base_fields | 252 | const baseFields = value.base_fields |
| 243 | const withdrawalFields = value.withdrawal_fields | 253 | const withdrawalFields = value.withdrawal_fields |
| 244 | - const resetMap = value.reset_map | ||
| 245 | const baseFieldsEmpty = Array.isArray(baseFields) && baseFields.length === 0 | 254 | const baseFieldsEmpty = Array.isArray(baseFields) && baseFields.length === 0 |
| 246 | const withdrawalFieldsEmpty = !Array.isArray(withdrawalFields) || withdrawalFields.length === 0 | 255 | const withdrawalFieldsEmpty = !Array.isArray(withdrawalFields) || withdrawalFields.length === 0 |
| 247 | - const resetMapEmpty = !resetMap || (typeof resetMap === 'object' && !Array.isArray(resetMap) && Object.keys(resetMap).length === 0) | 256 | + if (baseFieldsEmpty && withdrawalFieldsEmpty) { |
| 248 | - if (baseFieldsEmpty && withdrawalFieldsEmpty && resetMapEmpty) { | ||
| 249 | return fallbackRef | 257 | return fallbackRef |
| 250 | } | 258 | } |
| 259 | + // 处理字段中的 show_when 格式转换 | ||
| 260 | + const processedValue = processSchemaFields(value) | ||
| 261 | + return JSON.stringify(processedValue, null, 2).replace(/\n/g, '\n ') | ||
| 251 | } | 262 | } |
| 252 | if (typeof value === 'string') { | 263 | if (typeof value === 'string') { |
| 253 | return value | 264 | return value |
| ... | @@ -255,6 +266,42 @@ function buildSchemaCode(value, fallbackRef) { | ... | @@ -255,6 +266,42 @@ function buildSchemaCode(value, fallbackRef) { |
| 255 | return JSON.stringify(value, null, 2).replace(/\n/g, '\n ') | 266 | return JSON.stringify(value, null, 2).replace(/\n/g, '\n ') |
| 256 | } | 267 | } |
| 257 | 268 | ||
| 269 | +/** | ||
| 270 | + * 处理 Schema 字段,转换条件格式 | ||
| 271 | + * | ||
| 272 | + * @private | ||
| 273 | + */ | ||
| 274 | +function processSchemaFields(schema) { | ||
| 275 | + const result = {} | ||
| 276 | + for (const [key, value] of Object.entries(schema)) { | ||
| 277 | + if (key === 'base_fields' || key === 'withdrawal_fields') { | ||
| 278 | + result[key] = value.map(field => processFieldCondition(field)) | ||
| 279 | + } else if (key !== 'reset_map') { | ||
| 280 | + // 忽略 reset_map,不再生成 | ||
| 281 | + result[key] = value | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | + return result | ||
| 285 | +} | ||
| 286 | + | ||
| 287 | +/** | ||
| 288 | + * 处理字段条件,转换为新格式 | ||
| 289 | + * | ||
| 290 | + * @private | ||
| 291 | + */ | ||
| 292 | +function processFieldCondition(field) { | ||
| 293 | + const result = { ...field } | ||
| 294 | + // 转换 show_when: { field, equals } -> { field, op: 'eq', value } | ||
| 295 | + if (result.show_when && result.show_when.equals !== undefined) { | ||
| 296 | + result.show_when = { | ||
| 297 | + field: result.show_when.field, | ||
| 298 | + op: 'eq', | ||
| 299 | + value: result.show_when.equals | ||
| 300 | + } | ||
| 301 | + } | ||
| 302 | + return result | ||
| 303 | +} | ||
| 304 | + | ||
| 258 | function isEmptyObject(value) { | 305 | function isEmptyObject(value) { |
| 259 | if (!value || typeof value !== 'object' || Array.isArray(value)) { | 306 | if (!value || typeof value !== 'object' || Array.isArray(value)) { |
| 260 | return false | 307 | return false | ... | ... |
-
Please register or login to post a comment