hookehuyr

feat(plan): 合并选项控制功能到 develop

- 完善计划书字段选项控制功能
- 优化表单交互体验
- 提取字段配置模板
Showing 44 changed files with 1142 additions and 1551 deletions
......@@ -24,3 +24,7 @@ CLAUDE.md
*.pptx
.swc/
# Document parsing generated files
docs/parse-audit/
docs/parsed-backup/
......
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
---
## 📋 产品基本信息
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 产品名称 | 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日 | ✅ 请核对产品名称 |
| 产品类型 | savings | ✅ 请确认产品类型 |
| 币种 | USD | ✅ 请确认币种 |
| form_sn | `savings-product-ef3dd50b` | ✅ 请确认 form_sn 唯一性 |
| 缴费年期 | ["整付","3年","5年","10年","15年"] | ✅ 请确认缴费年期选项 |
| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 |
| 保险期间 | 终身 | ✅ 请确认保险期间 |
### 💰 储蓄类产品特有字段
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 提取方式 | ["最高固定提取金额"] | ✅ 请确认提取方式 |
| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 |
---
## 🤖 智能字段提取报告
### 匹配统计
- ✅ 成功匹配: 4 字段
- ⚠️ 使用默认值: 3 字段
- ❌ 未匹配(需人工补充): 0 字段
### ✅ 已成功匹配的字段
- product_name
- product_type
- payment_periods
- withdrawal_modes
### ⚠️ 使用默认值的字段
- **currency**: 未找到字段 "currency",使用默认值: "USD"
- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75}
- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身"
---
## 🧾 配置预览
```javascript
{
"product_name": "宏摯傳承保障計劃 - 性別, 年齡, 出生年月日",
"product_type": "savings",
"currency": "USD",
"form_sn": "savings-product-ef3dd50b",
"payment_periods": [
"整付",
"3年",
"5年",
"10年",
"15年"
],
"age_range": {
"min": 0,
"max": 75
},
"insurance_period": "终身",
"is_savings": true,
"withdrawal_modes": [
"最高固定提取金额"
],
"withdrawal_periods": [
"1年",
"3年",
"5年",
"10年"
]
}
```
---
## 📝 表单字段 (form_schema)
```javascript
{
"base_fields": [],
"withdrawal_fields": [],
"reset_map": {}
}
```
---
## 🔄 提交字段映射 (submit_mapping)
```javascript
{}
```
---
## 🧩 生成配置片段
```javascript
/**
* 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日
* @added 2026-02-15T02:20:30.982Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-ef3dd50b': {
name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: ["整付","3年","5年","10年","15年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD',
withdrawal_modes: ["最高固定提取金额"],
withdrawal_periods: ["1年","3年","5年","10年"]
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
```
---
## ✅ 审核检查清单
### 基础信息
- [ ] 产品名称正确
- [ ] 产品类型正确(savings/critical-illness/life-insurance)
- [ ] 币种正确(USD/CNY/HKD/EUR)
- [ ] form_sn 唯一且符合命名规范
### 缴费与年龄
- [ ] 缴费年期选项完整且正确
- [ ] 年龄范围合理
- [ ] 保险期间正确
### 储蓄类特有(如适用)
- [ ] 提取方式正确
- [ ] 提取期选项完整
- [ ] 表单字段定义完整
- [ ] 提交字段映射正确
---
## 📋 审核后操作
### 确认无误
```bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md \
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md
```
### 需要修改
1. 编辑本文件修正内容
2. 重新提交审核
### 放弃本次解析
```bash
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯傳承保障計劃-性別-年齡-出生年月日.md
```
---
## 审核状态
- [ ] 待审核
- [ ] 已通过
- [ ] 已拒绝
## 审核意见
```text
```
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
---
## 📋 产品基本信息
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 产品名称 | 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 | ✅ 请核对产品名称 |
| 产品类型 | savings | ✅ 请确认产品类型 |
| 币种 | USD | ✅ 请确认币种 |
| form_sn | `savings-product-aaaa60f8` | ✅ 请确认 form_sn 唯一性 |
| 缴费年期 | ["整付","3年","5年"] | ✅ 请确认缴费年期选项 |
| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 |
| 保险期间 | 终身 | ✅ 请确认保险期间 |
### 💰 储蓄类产品特有字段
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 |
| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 |
---
## 🤖 智能字段提取报告
### 匹配统计
- ✅ 成功匹配: 2 字段
- ⚠️ 使用默认值: 4 字段
- ❌ 未匹配(需人工补充): 0 字段
### ✅ 已成功匹配的字段
- product_name
- payment_periods
### ⚠️ 使用默认值的字段
- **product_type**: 未找到字段 "product_type",使用默认值: "savings"
- **currency**: 未找到字段 "currency",使用默认值: "USD"
- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75}
- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身"
---
## 🧾 配置预览
```javascript
{
"product_name": "宏摯家傳承保險計劃- 性別, 年齡, 出生年月日",
"product_type": "savings",
"currency": "USD",
"form_sn": "savings-product-aaaa60f8",
"payment_periods": [
"整付",
"3年",
"5年"
],
"age_range": {
"min": 0,
"max": 75
},
"insurance_period": "终身",
"is_savings": true,
"withdrawal_modes": [
"年龄指定金额",
"最高固定金额"
],
"withdrawal_periods": [
"1年",
"3年",
"5年",
"10年"
]
}
```
---
## 📝 表单字段 (form_schema)
```javascript
{
"base_fields": [],
"withdrawal_fields": [],
"reset_map": {}
}
```
---
## 🔄 提交字段映射 (submit_mapping)
```javascript
{}
```
---
## 🧩 生成配置片段
```javascript
/**
* 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
* @added 2026-02-15T02:20:30.997Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-aaaa60f8': {
name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: ["整付","3年","5年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD',
withdrawal_modes: ["年龄指定金额","最高固定金额"],
withdrawal_periods: ["1年","3年","5年","10年"]
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
```
---
## ✅ 审核检查清单
### 基础信息
- [ ] 产品名称正确
- [ ] 产品类型正确(savings/critical-illness/life-insurance)
- [ ] 币种正确(USD/CNY/HKD/EUR)
- [ ] form_sn 唯一且符合命名规范
### 缴费与年龄
- [ ] 缴费年期选项完整且正确
- [ ] 年龄范围合理
- [ ] 保险期间正确
### 储蓄类特有(如适用)
- [ ] 提取方式正确
- [ ] 提取期选项完整
- [ ] 表单字段定义完整
- [ ] 提交字段映射正确
---
## 📋 审核后操作
### 确认无误
```bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md \
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md
```
### 需要修改
1. 编辑本文件修正内容
2. 重新提交审核
### 放弃本次解析
```bash
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏摯家傳承保險計劃-性別-年齡-出生年月日.md
```
---
## 审核状态
- [ ] 待审核
- [ ] 已通过
- [ ] 已拒绝
## 审核意见
```text
```
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
---
## 📋 产品基本信息
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 产品名称 | 宏浚傳承保障計劃 | ✅ 请核对产品名称 |
| 产品类型 | savings | ✅ 请确认产品类型 |
| 币种 | USD | ✅ 请确认币种 |
| form_sn | `savings-product-d1581522` | ✅ 请确认 form_sn 唯一性 |
| 缴费年期 | ["整付","2年","5年"] | ✅ 请确认缴费年期选项 |
| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 |
| 保险期间 | 终身 | ✅ 请确认保险期间 |
### 💰 储蓄类产品特有字段
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 |
| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 |
---
## 🤖 智能字段提取报告
### 匹配统计
- ✅ 成功匹配: 2 字段
- ⚠️ 使用默认值: 4 字段
- ❌ 未匹配(需人工补充): 0 字段
### ✅ 已成功匹配的字段
- product_name
- payment_periods
### ⚠️ 使用默认值的字段
- **product_type**: 未找到字段 "product_type",使用默认值: "savings"
- **currency**: 未找到字段 "currency",使用默认值: "USD"
- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75}
- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身"
---
## 🧾 配置预览
```javascript
{
"product_name": "宏浚傳承保障計劃",
"product_type": "savings",
"currency": "USD",
"form_sn": "savings-product-d1581522",
"payment_periods": [
"整付",
"2年",
"5年"
],
"age_range": {
"min": 0,
"max": 75
},
"insurance_period": "终身",
"is_savings": true,
"withdrawal_modes": [
"年龄指定金额",
"最高固定金额"
],
"withdrawal_periods": [
"1年",
"3年",
"5年",
"10年"
]
}
```
---
## 📝 表单字段 (form_schema)
```javascript
{
"base_fields": [],
"withdrawal_fields": [],
"reset_map": {}
}
```
---
## 🔄 提交字段映射 (submit_mapping)
```javascript
{}
```
---
## 🧩 生成配置片段
```javascript
/**
* 宏浚傳承保障計劃
* @added 2026-02-15T02:20:30.997Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-d1581522': {
name: '宏浚傳承保障計劃',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: ["整付","2年","5年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD',
withdrawal_modes: ["年龄指定金额","最高固定金额"],
withdrawal_periods: ["1年","3年","5年","10年"]
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
```
---
## ✅ 审核检查清单
### 基础信息
- [ ] 产品名称正确
- [ ] 产品类型正确(savings/critical-illness/life-insurance)
- [ ] 币种正确(USD/CNY/HKD/EUR)
- [ ] form_sn 唯一且符合命名规范
### 缴费与年龄
- [ ] 缴费年期选项完整且正确
- [ ] 年龄范围合理
- [ ] 保险期间正确
### 储蓄类特有(如适用)
- [ ] 提取方式正确
- [ ] 提取期选项完整
- [ ] 表单字段定义完整
- [ ] 提交字段映射正确
---
## 📋 审核后操作
### 确认无误
```bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md \
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md
```
### 需要修改
1. 编辑本文件修正内容
2. 重新提交审核
### 放弃本次解析
```bash
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-宏浚傳承保障計劃.md
```
---
## 审核状态
- [ ] 待审核
- [ ] 已通过
- [ ] 已拒绝
## 审核意见
```text
```
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
---
## 📋 产品基本信息
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 产品名称 | 赤霞珠終身壽險計劃2基本人壽保障選項 | ✅ 请核对产品名称 |
| 产品类型 | savings | ✅ 请确认产品类型 |
| 币种 | USD | ✅ 请确认币种 |
| form_sn | `savings-2-031c1237` | ✅ 请确认 form_sn 唯一性 |
| 缴费年期 | ["5年","8年","12年","15年"] | ✅ 请确认缴费年期选项 |
| 年龄范围 | 0-75岁 | ✅ 请确认年龄范围 |
| 保险期间 | 终身 | ✅ 请确认保险期间 |
### 💰 储蓄类产品特有字段
| 字段 | 提取值 | 需要确认 |
|------|--------|---------|
| 提取方式 | ["年龄指定金额","最高固定金额"] | ✅ 请确认提取方式 |
| 提取期 | ["1年","3年","5年","10年"] | ✅ 请确认提取期选项 |
---
## 🤖 智能字段提取报告
### 匹配统计
- ✅ 成功匹配: 2 字段
- ⚠️ 使用默认值: 4 字段
- ❌ 未匹配(需人工补充): 0 字段
### ✅ 已成功匹配的字段
- product_name
- payment_periods
### ⚠️ 使用默认值的字段
- **product_type**: 未找到字段 "product_type",使用默认值: "savings"
- **currency**: 未找到字段 "currency",使用默认值: "USD"
- **age_range**: 未找到字段 "age_range",使用默认值: {"min":0,"max":75}
- **insurance_period**: 未找到字段 "insurance_period",使用默认值: "终身"
---
## 🧾 配置预览
```javascript
{
"product_name": "赤霞珠終身壽險計劃2基本人壽保障選項",
"product_type": "savings",
"currency": "USD",
"form_sn": "savings-2-031c1237",
"payment_periods": [
"5年",
"8年",
"12年",
"15年"
],
"age_range": {
"min": 0,
"max": 75
},
"insurance_period": "终身",
"is_savings": true,
"withdrawal_modes": [
"年龄指定金额",
"最高固定金额"
],
"withdrawal_periods": [
"1年",
"3年",
"5年",
"10年"
]
}
```
---
## 📝 表单字段 (form_schema)
```javascript
{
"base_fields": [],
"withdrawal_fields": [],
"reset_map": {}
}
```
---
## 🔄 提交字段映射 (submit_mapping)
```javascript
{}
```
---
## 🧩 生成配置片段
```javascript
/**
* 赤霞珠終身壽險計劃2基本人壽保障選項
* @added 2026-02-15T02:20:30.997Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-2-031c1237': {
name: '赤霞珠終身壽險計劃2基本人壽保障選項',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: ["5年","8年","12年","15年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD',
withdrawal_modes: ["年龄指定金额","最高固定金额"],
withdrawal_periods: ["1年","3年","5年","10年"]
},
form_schema: savingsFormSchema,
submit_mapping: savingsSubmitMapping
}
}
```
---
## ✅ 审核检查清单
### 基础信息
- [ ] 产品名称正确
- [ ] 产品类型正确(savings/critical-illness/life-insurance)
- [ ] 币种正确(USD/CNY/HKD/EUR)
- [ ] form_sn 唯一且符合命名规范
### 缴费与年龄
- [ ] 缴费年期选项完整且正确
- [ ] 年龄范围合理
- [ ] 保险期间正确
### 储蓄类特有(如适用)
- [ ] 提取方式正确
- [ ] 提取期选项完整
- [ ] 表单字段定义完整
- [ ] 提交字段映射正确
---
## 📋 审核后操作
### 确认无误
```bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md \
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md
```
### 需要修改
1. 编辑本文件修正内容
2. 重新提交审核
### 放弃本次解析
```bash
rm docs/parse-audit/pending/计划书模版2/2026-02-15-计划书模版2-赤霞珠終身壽險計劃2基本人壽保障選項.md
```
---
## 审核状态
- [ ] 待审核
- [ ] 已通过
- [ ] 已拒绝
## 审核意见
```text
```
{"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"}
{"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"}
{"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"}
{"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"}
{"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"}
{"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"}
{"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"}
This diff is collapsed. Click to expand it.
/**
* 计划书模版配置
*
* @description 定义产品 form_sn 到模版组件和配置的映射关系
* @module config/plan-templates
* @author Claude Code
* @created 2026-02-06
* @updated 2026-02-13 - 新增文档解析工具入口
*
* --- 快速添加新产品(开发工具) ---
* 开发环境可使用以下工具快速添加新产品配置:
* 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析)
* 2. API 配置工具:/admin/document-parser/config (配置 AI 服务)
*
* 使用方式:
* - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件
*
* --- 手动添加步骤 ---
* 1. 找到对应的产品分类(人寿/重疾/储蓄)
* 2. 复制现有配置作为模板
* 3. 修改 name, currency, payment_periods, age_range 等字段
* 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号)
*/
/**
* 计划书模版配置映射
* @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版
*
* @example
* // 产品 API 返回
* {
* id: 1,
* product_name: "WIOP3E 盈传创富保障计划 3 - 优选版",
* form_sn: "life-insurance-wiop3e" // 对应下面的配置 key
* }
*/
export const PLAN_TEMPLATES = {
// 人寿保险产品 - WIOP3E
'life-insurance-wiop3e': {
name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD', // 币种:USD/CNY/HKD/EUR
payment_periods: [
// 缴费年期选项
'整付(0-75 岁)',
'5 年(0-70 岁)',
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 }, // 年龄范围
insurance_period: '终身' // 保险期间
}
},
// 人寿保险产品 - WIOP3
'life-insurance-wiop3': {
name: 'WIOP3 - 盈传创富保障计划 3',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: [
'整付(0-75 岁)',
'5 年(0-70 岁)',
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MPC
'critical-illness-mpc': {
name: 'MPC 守护无间重疾',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MBC PRO
'critical-illness-mbc-pro': {
name: 'MBC PRO 活跃人生重疾保 PRO',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MBC2
'critical-illness-mbc2': {
name: 'MBC2 活跃人生重疾保 2',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// ====== 储蓄型产品(统一逻辑) ======
// GS - 宏挚传承保障计划
'savings-gs': {
name: '宏挚传承保障计划',
component: 'SavingsTemplate',
category: 'savings', // 储蓄型产品
config: {
currency: 'USD', // 默认美元
payment_periods: [
'整付',
'3 年',
'5 年',
'10 年',
'15 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
// 提取计划配置
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
default_currency: 'USD', // 统一为美元
withdrawal_modes: [
'年龄指定金额', // 方式1
'最高固定金额' // 方式2
],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// GC - 宏挚家传保险计划
'savings-gc': {
name: '宏挚家传保险计划',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'整付',
'3 年',
'5 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// FA - 宏浚传承保障计划
'savings-fa': {
name: '宏浚传承保障计划',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'整付',
'2 年',
'5 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险)
'savings-lv2': {
name: '赤霞珠终身寿险计划2',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'5 年',
'8 年',
'12 年',
'15 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
}
}
/**
* 全局功能开关
* @description 用于控制实验性功能或未来扩展功能的开关
*
* @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true
*/
export const FEATURE_FLAGS = {
/**
* 多币种切换功能
* @description false: 方案 1 - 固定币种(当前实现)
* true: 方案 2 - 支持多币种切换(未来扩展)
* @type {boolean}
*/
MULTI_CURRENCY_ENABLED: false
}
/**
* 币种符号映射
* @description 币种代码到符号的映射关系
*/
export const CURRENCY_SYMBOLS = {
CNY: '¥', // 人民币
USD: '$', // 美元
HKD: 'HK$', // 港币
EUR: '€' // 欧元
}
/**
* 币种完整信息映射
* @description 币种代码到完整信息的映射(用于多币种模式)
*/
export const CURRENCY_MAP = {
CNY: { label: '人民币', symbol: '¥', value: 'CNY' },
USD: { label: '美元', symbol: '$', value: 'USD' },
HKD: { label: '港币', symbol: 'HK$', value: 'HKD' },
EUR: { label: '欧元', symbol: '€', value: 'EUR' }
}
/**
* 根据 form_sn 获取模版配置
* @param {string} formSn - 产品 API 返回的 form_sn 字段
* @returns {Object|null} 模版配置对象,未找到返回 null
*
* @example
* const config = getTemplateConfig('life-insurance-wiop3e')
* // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} }
*/
export function getTemplateConfig(formSn) {
if (!formSn) {
console.warn('[plan-templates] form_sn 为空')
return null
}
const config = PLAN_TEMPLATES[formSn]
if (!config) {
console.error(`[plan-templates] 未找到模版配置: ${formSn}`)
return null
}
return config
}
/**
* 获取币种符号
* @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR)
* @returns {string} 币种符号
*
* @example
* const symbol = getCurrencySymbol('USD') // 返回: '$'
*/
export function getCurrencySymbol(currencyCode) {
return CURRENCY_SYMBOLS[currencyCode] || '¥'
}
/**
* 计划书模版配置
*
* @description 定义产品 form_sn 到模版组件和配置的映射关系
* @module config/plan-templates
* @author Claude Code
* @created 2026-02-06
* @updated 2026-02-13 - 新增文档解析工具入口
*
* --- 快速添加新产品(开发工具) ---
* 开发环境可使用以下工具快速添加新产品配置:
* 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析)
* 2. API 配置工具:/admin/document-parser/config (配置 AI 服务)
*
* 使用方式:
* - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件
*
* --- 手动添加步骤 ---
* 1. 找到对应的产品分类(人寿/重疾/储蓄)
* 2. 复制现有配置作为模板
* 3. 修改 name, currency, payment_periods, age_range 等字段
* 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号)
*/
/**
* 计划书模版配置映射
* @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版
*
* @example
* // 产品 API 返回
* {
* id: 1,
* product_name: "WIOP3E 盈传创富保障计划 3 - 优选版",
* form_sn: "life-insurance-wiop3e" // 对应下面的配置 key
* }
*/
export const PLAN_TEMPLATES = {
// 人寿保险产品 - WIOP3E
'life-insurance-wiop3e': {
name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD', // 币种:USD/CNY/HKD/EUR
payment_periods: [
// 缴费年期选项
'整付(0-75 岁)',
'5 年(0-70 岁)',
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 }, // 年龄范围
insurance_period: '终身' // 保险期间
}
},
// 人寿保险产品 - WIOP3
'life-insurance-wiop3': {
name: 'WIOP3 - 盈传创富保障计划 3',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: [
'整付(0-75 岁)',
'5 年(0-70 岁)',
'10 年(0-70 岁)'
],
age_range: { min: 0, max: 75 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MPC
'critical-illness-mpc': {
name: 'MPC 守护无间重疾',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MBC PRO
'critical-illness-mbc-pro': {
name: 'MBC PRO 活跃人生重疾保 PRO',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// 重疾保险产品 - MBC2
'critical-illness-mbc2': {
name: 'MBC2 活跃人生重疾保 2',
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
'25 年(15 日 - 60 岁)'
],
age_range: { min: 0, max: 65 },
insurance_period: '终身'
}
},
// ====== 储蓄型产品(统一逻辑) ======
// GS - 宏挚传承保障计划
'savings-gs': {
name: '宏挚传承保障计划',
component: 'SavingsTemplate',
category: 'savings', // 储蓄型产品
config: {
currency: 'USD', // 默认美元
payment_periods: [
'整付',
'3 年',
'5 年',
'10 年',
'15 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
// 提取计划配置
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
default_currency: 'USD', // 统一为美元
withdrawal_modes: [
'年龄指定金额', // 方式1
'最高固定金额' // 方式2
],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// GC - 宏挚家传保险计划
'savings-gc': {
name: '宏挚家传保险计划',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'整付',
'3 年',
'5 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// FA - 宏浚传承保障计划
'savings-fa': {
name: '宏浚传承保障计划',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'整付',
'2 年',
'5 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
},
// LV2 - 赤霞珠终身寿险计划2(储蓄型终身寿险)
'savings-lv2': {
name: '赤霞珠终身寿险计划2',
component: 'SavingsTemplate',
category: 'savings',
config: {
currency: 'USD',
payment_periods: [
'5 年',
'8 年',
'12 年',
'15 年',
],
age_range: { min: 0, max: 100 },
insurance_period: '终身',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
'2年',
'3年',
'5年',
'10年',
'15年',
'20年',
'终身'
]
}
}
}
}
/**
* 全局功能开关
* @description 用于控制实验性功能或未来扩展功能的开关
*
* @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true
*/
export const FEATURE_FLAGS = {
/**
* 多币种切换功能
* @description false: 方案 1 - 固定币种(当前实现)
* true: 方案 2 - 支持多币种切换(未来扩展)
* @type {boolean}
*/
MULTI_CURRENCY_ENABLED: false
}
/**
* 币种符号映射
* @description 币种代码到符号的映射关系
*/
export const CURRENCY_SYMBOLS = {
CNY: '¥', // 人民币
USD: '$', // 美元
HKD: 'HK$', // 港币
EUR: '€' // 欧元
}
/**
* 币种完整信息映射
* @description 币种代码到完整信息的映射(用于多币种模式)
*/
export const CURRENCY_MAP = {
CNY: { label: '人民币', symbol: '¥', value: 'CNY' },
USD: { label: '美元', symbol: '$', value: 'USD' },
HKD: { label: '港币', symbol: 'HK$', value: 'HKD' },
EUR: { label: '欧元', symbol: '€', value: 'EUR' }
}
/**
* 根据 form_sn 获取模版配置
* @param {string} formSn - 产品 API 返回的 form_sn 字段
* @returns {Object|null} 模版配置对象,未找到返回 null
*
* @example
* const config = getTemplateConfig('life-insurance-wiop3e')
* // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} }
*/
export function getTemplateConfig(formSn) {
if (!formSn) {
console.warn('[plan-templates] form_sn 为空')
return null
}
const config = PLAN_TEMPLATES[formSn]
if (!config) {
console.error(`[plan-templates] 未找到模版配置: ${formSn}`)
return null
}
return config
}
/**
* 获取币种符号
* @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR)
* @returns {string} 币种符号
*
* @example
* const symbol = getCurrencySymbol('USD') // 返回: '$'
*/
export function getCurrencySymbol(currencyCode) {
return CURRENCY_SYMBOLS[currencyCode] || '¥'
}
# 字段条件显示系统扩展计划
## 背景与目标
### 当前问题
- `show_when` 只支持简单的等于比较 (`equals`)
- 不支持 OR 条件、嵌套条件
- 不支持不等于、大于、小于等操作符
- 隐藏字段的清理逻辑分散在 `reset_map`
- 提交时可能包含隐藏字段的脏数据
### 目标
构建一个声明式的条件规则系统,支持复杂逻辑同时保持配置可读性。
## 技术方案:方案 B - 条件规则引擎
### 核心文件变更
| 文件 | 操作 | 说明 |
|------|------|------|
| `src/config/plan-conditions.js` | 新建 | 条件操作符和评估引擎 |
| `src/composables/useFieldDependencies.js` | 修改 | 集成新的条件评估器 |
| `src/composables/usePlanSubmit.js` | 新建 | 提交时字段过滤逻辑 |
| `src/config/plan-templates.js` | 修改 | 迁移现有配置到新格式 |
| `src/composables/__tests__/plan-conditions.test.js` | 新建 | 条件引擎单元测试 |
---
## 阶段 1:核心条件引擎
### 任务 1.1 创建条件操作符定义
- [x] 创建 `src/config/plan-conditions.js`
- [x] 定义比较操作符:`eq`, `ne`, `gt`, `gte`, `lt`, `lte`
- [x] 定义集合操作符:`in`, `nin`
- [x] 定义字符串操作符:`contains`, `startsWith`, `matches`
- [x] 定义布尔操作符:`truthy`, `falsy`, `empty`, `notEmpty`
- [x] 编写单元测试验证操作符正确性
### 任务 1.2 实现条件评估函数
- [x] 实现 `evaluateCondition(condition, formData)` 函数
- [x] 支持简单条件:`{ field, op, value }`
- [x] 支持 AND 逻辑:`{ and: [...] }`
- [x] 支持 OR 逻辑:`{ or: [...] }`
- [x] 支持 NOT 逻辑:`{ not: {...} }`
- [x] 支持嵌套条件组合
- [x] 编写单元测试覆盖各种条件场景
### 任务 1.3 向后兼容处理
- [x] 支持旧格式 `show_when: { field: 'x', equals: 'y' }`
- [x] 支持旧格式数组 `show_when: [{ field: 'x', equals: 'y' }]`
- [x] 自动转换为新格式
---
## 阶段 2:清理机制
### 任务 2.1 字段清理规则
- [x] 在字段定义中添加 `clear_when_hidden` 属性
- [x] 实现 `clear_when_hidden: true` 自动清空
- [x] 实现 `clear_when_hidden: false` 保留值
- [x] 实现级联清理 `clear_when_hidden: { clear_dependents: [...] }`
### 任务 2.2 提交时字段过滤
- [x] 实现 `filterHiddenFields(formData, visibleFields)` 函数
- [x] 只提交当前可见的字段
- [x] 保持 API 兼容性
### 任务 2.3 更新 useFieldDependencies
- [x] 集成新的条件评估引擎
- [x] 实现字段隐藏时的自动清理
- [x] 更新 `isFieldVisible` 使用新引擎
- [x] 保持 API 兼容性
---
## 阶段 3:配置迁移
### 任务 3.1 迁移储蓄类模板
- [x] 迁移 `savingsFormSchema.withdrawal_fields` 到新格式
- [x] 删除 `reset_map`,使用 `clear_when_hidden` 替代
- [x] 验证功能正常(测试通过 160/160)
### 任务 3.2 迁移保障类模板
- [x] 检查 `protectionFormSchema` 是否需要条件
- [x] 按需添加条件规则(保障类暂无条件需求,无需迁移)
### 任务 3.3 更新文档解析工具 ⚠️ 重要
- [x] 更新 `src/utils/parsers/config-generator.js` 生成新格式配置
- [x] 修改 `show_when` 生成逻辑,使用 `{ field, op, value }` 格式
- [x] 停止生成 `reset_map`(已忽略)
- [x] 测试通过(160/160)
**说明**:MCP 文档解析服务保持不变,仅更新配置生成器输出格式。
### 任务 3.4 更新文档
- [x] 更新 `plan-templates.js` 顶部的使用说明
- [x] 添加条件规则配置示例(已在 plan-templates.js 注释中说明)
- [x] 添加常见场景示例(已在 savingsFormSchema 中实现)
---
## 阶段 4:测试与验证
### 任务 4.1 单元测试
- [ ] 条件操作符测试覆盖率 > 90%
- [ ] 条件评估引擎测试覆盖率 > 90%
- [ ] useFieldDependencies 测试更新
### 任务 4.2 集成测试
- [ ] 测试储蓄类产品完整流程
- [ ] 测试字段显示/隐藏切换
- [ ] 测试提交时字段过滤
- [ ] 测试向后兼容性
### 任务 4.3 真机验证
- [ ] 微信开发者工具验证
- [ ] 检查性能影响
- [ ] 检查内存占用
---
## 配置格式示例
### 新格式示例
```javascript
// 简单条件
show_when: { field: 'smoker', op: 'eq', value: '是' }
// 多条件 AND
show_when: {
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 30 }
]
}
// OR 条件
show_when: {
or: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gt', value: 50 }
]
}
// 嵌套条件
show_when: {
and: [
{ field: 'product_type', op: 'in', value: ['A', 'B'] },
{
or: [
{ field: 'coverage', op: 'gte', value: 100000 },
{ field: 'payment_period', op: 'eq', value: '20年' }
]
}
]
}
// 清理规则
{
id: 'withdrawal_amount',
show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' },
clear_when_hidden: true // 隐藏时清空
}
```
### 旧格式(保持兼容)
```javascript
// 旧格式仍然支持
show_when: { field: 'withdrawal_mode', equals: '指定提取金额' }
show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }]
```
---
## 风险与注意事项
1. **向后兼容**:必须保持现有配置格式可用
2. **性能**:条件评估不能影响表单响应速度
3. **可读性**:配置文件需要保持可理解性
4. **测试覆盖**:每个操作符都需要充分测试
---
## 进度追踪
| 阶段 | 状态 | 完成时间 |
|------|------|---------|
| 阶段 1:核心引擎 | ✅ 完成 | 2026-02-15 |
| 阶段 2:清理机制 | ✅ 完成 | 2026-02-15 |
| 阶段 3:配置迁移 | ✅ 完成 | 2026-02-15 |
| 阶段 4:测试验证 | 🔄 待真机验证 | - |
---
**创建时间**: 2026-02-15
**预计工期**: 3-4 天
**维护者**: Claude Code
......@@ -9,8 +9,8 @@
## 📊 总体进度
- [x] **第 1 步**: 目标与输出定义
- [ ] **第 2 步**: 文本抽取管线
- [ ] **第 3 步**: 结构化解析与校验
- [x] **第 2 步**: 文本抽取管线
- [x] **第 3 步**: 结构化解析与校验
- [x] **第 4 步**: 生成与写入稳态化
- [x] **第 5 步**: 测试与验证
- [x] **第 6 步**: 运营与审计
......
......@@ -29,15 +29,15 @@
"postinstall": "weapp-tw patch",
"lint": "eslint --ext .js,.vue src",
"test": "vitest run",
"api:generate": "node scripts/generateApiFromOpenAPI.js",
"changelog:check": "bash scripts/check-changelog.sh 7",
"changelog:check:30": "bash scripts/check-changelog.sh 30",
"changelog:check:all": "bash scripts/check-changelog.sh 0",
"api:generate": "node scripts/api-generator/generateApiFromOpenAPI.js",
"changelog:check": "bash scripts/changelog/check-changelog.sh 7",
"changelog:check:30": "bash scripts/changelog/check-changelog.sh 30",
"changelog:check:all": "bash scripts/changelog/check-changelog.sh 0",
"prepare": "husky",
"parse:docs": "node scripts/parse-docs.js",
"parse:docs:list": "node scripts/parse-docs.js --list",
"parse:docs:status": "node scripts/parse-docs.js --status",
"parse:docs:file": "node scripts/parse-docs.js --file=",
"parse:docs": "node scripts/doc-parser/parse-docs.js",
"parse:docs:list": "node scripts/doc-parser/parse-docs.js --list",
"parse:docs:status": "node scripts/doc-parser/parse-docs.js --status",
"parse:docs:file": "node scripts/doc-parser/parse-docs.js --file=",
"release": "standard-version"
},
"browserslist": [
......
# Scripts 目录说明
> 最后更新: 2026-02-15
本目录包含项目的各类开发工具脚本,按功能分组存放在子目录中。
## 📁 目录结构
```
scripts/
├── api-generator/ # API 代码生成工具
├── doc-parser/ # 文档解析工具
├── changelog/ # CHANGELOG 管理工具
└── README.md # 本说明文件
```
---
## 🚀 快速使用
### API 代码生成
```bash
pnpm api:generate # 从 OpenAPI 生成 API 接口
```
### 文档解析
```bash
pnpm parse:docs # 解析待处理文档
pnpm parse:docs:list # 查看待处理文档
pnpm parse:docs:status # 查看解析状态
```
### CHANGELOG 检查
```bash
pnpm changelog:check # 检查最近 7 天的漏记
pnpm changelog:check:30 # 检查最近 30 天的漏记
```
---
## 📦 子目录详情
| 目录 | 用途 | 相关 npm scripts |
|------|------|-----------------|
| `api-generator/` | 从 OpenAPI 规范生成 JavaScript API 代码 | `api:generate` |
| `doc-parser/` | 解析 PDF/DOCX 等文档,智能提取配置字段 | `parse:docs:*` |
| `changelog/` | 检查和归档 CHANGELOG 记录 | `changelog:check:*` |
---
## 🔗 相关文档
- [API 生成指南](./api-generator/GUIDE.md)
- [文档解析快速开始](./doc-parser/QUICKSTART.md)
- [CHANGELOG 规范](../docs/CHANGELOG.md)
# API 代码生成工具
从 OpenAPI 规范自动生成 JavaScript API 接口代码。
## 📁 文件说明
| 文件 | 说明 | 使用频率 |
|------|------|---------|
| `generateApiFromOpenAPI.js` | 主脚本 - 从 OpenAPI 生成 API 代码 | ⭐⭐⭐ 高频 |
| `apiDiff.js` | API 对比工具 - 检测 API 变更和破坏性改动 | ⭐⭐ 中频 |
| `test-generate.js` | 测试脚本 - 验证生成的 API 文件是否正确 | ⭐ 低频 |
| `API_GUIDE.md` | 使用指南 - 详细的 API 维护工作流文档 | 📖 文档 |
## 🚀 使用方式
```bash
# 生成所有 API 接口
pnpm api:generate
# 生成指定模块
pnpm api:generate -- --module=user
```
## 📖 详细文档
参见 [API_GUIDE.md](./API_GUIDE.md) 了解完整的 API 维护工作流。
## 🔧 工作原理
1. 扫描 `docs/api-specs/` 目录下的 OpenAPI 文档
2. 解析 Markdown 文件中的 YAML 规范
3. 检测 API 变更(新增、修改、删除)
4. 生成符合项目规范的 JavaScript API 代码
5. 保存到 `src/api/` 目录
## 📝 生成代码示例
输入 (OpenAPI YAML):
```yaml
paths:
/user/info:
get:
summary: 获取用户信息
parameters:
- name: user_id
in: query
```
输出 (JavaScript):
```javascript
/**
* 获取用户信息
* @param {Object} params
* @param {string} params.user_id - 用户ID
*/
export const getUserInfoAPI = (params) => {
return buildApiUrl('user_info', params)
}
```
# CHANGELOG 管理工具
检查和归档项目的 CHANGELOG 记录。
## 📁 文件说明
| 文件 | 说明 | 使用频率 |
|------|------|---------|
| `check-changelog.sh` | 漏记检查 - 扫描 git 提交,检查 CHANGELOG 漏记 | ⭐⭐⭐ 高频 |
| `archive-changelog.sh` | 归档脚本 - 当记录超过 20 条时自动归档 | ⭐ 低频 |
## 🚀 使用方式
### 检查漏记
```bash
# 检查最近 7 天(默认)
pnpm changelog:check
# 检查最近 30 天
pnpm changelog:check:30
# 检查所有提交
pnpm changelog:check:all
# 直接运行
./scripts/changelog/check-changelog.sh 7
```
### 归档旧记录
```bash
# 直接运行归档脚本
./scripts/changelog/archive-changelog.sh
```
## 📋 检查输出示例
```
======================================
CHANGELOG 漏记检查工具
======================================
检查范围: 最近 7 天
[1/4] 正在获取 git 提交记录...
找到 5 个提交
[2/4] 正在读取 CHANGELOG 记录...
找到 3 条记录
[3/4] 正在对比...
[4/4] 生成报告...
⚠️ 发现 2 处漏记:
1. feat(plan): 添加计划书表单验证
提交: abc1234 (2026-02-14)
建议: 在 CHANGELOG 中添加此功能记录
2. fix(login): 修复登录跳转问题
提交: def5678 (2026-02-13)
建议: 在 CHANGELOG 中添加此修复记录
```
## 🔧 归档规则
`docs/CHANGELOG.md` 记录数超过 20 条时:
1. 自动将旧记录移动到 `docs/changelog-archive/`
2. 主文件只保留最近 20 条记录
3. 归档文件以日期命名:`CHANGELOG-archive-YYYYMMDD.md`
## 📝 最佳实践
1. **每次提交后检查**:运行 `pnpm changelog:check` 确保没有漏记
2. **及时记录**:在提交代码时同步更新 CHANGELOG
3. **定期归档**:每月运行一次归档脚本
# 文档解析工具
从 PDF、DOCX 等文档中智能提取配置字段,自动生成计划书模板配置。
## 📁 文件说明
| 文件 | 说明 | 使用频率 |
|------|------|---------|
| `parse-docs.js` | 主脚本 - 文档解析和配置生成 | ⭐⭐⭐ 高频 |
| `smart-field-extractor.js` | 智能字段提取器 - 从文档中提取表单字段 | ⭐⭐ 中频 |
| `product-splitter.js` | 产品分割器 - 识别和分割多产品文档 | ⭐⭐ 中频 |
| `parse-config.js` | 配置文件 - markitdown 和 AI 服务配置 | 📋 配置 |
| `parse-docs.test.js` | 测试文件 - 单元测试 | 🧪 测试 |
| `QUICKSTART.md` | 快速开始指南 | 📖 文档 |
## 🚀 使用方式
```bash
# 解析所有待处理文档
pnpm parse:docs
# 查看待处理文档列表
pnpm parse:docs:list
# 查看解析状态
pnpm parse:docs:status
# 解析指定文件
pnpm parse:docs -- --file=产品说明书.pdf
# 应用审核通过的配置
pnpm parse:docs -- --apply=计划书模版4
# 预览应用配置(不实际修改)
pnpm parse:docs -- --apply=计划书模版4 --dry-run
```
## 📖 详细文档
参见 [QUICKSTART.md](./QUICKSTART.md) 了解完整的快速开始指南。
## 🔧 工作原理
1. 扫描 `docs/to-parse/` 目录下的待处理文档
2. 使用 markitdown 将文档转换为 Markdown
3. 调用 AI 服务提取配置字段
4. 生成可审核的配置文件
5. 审核通过后应用到 `src/config/plan-templates.js`
## 📝 支持的文档格式
- PDF (`.pdf`)
- Word (`.doc`, `.docx`)
- 文本 (`.txt`, `.md`)
## 🤖 AI 配置
`.env` 文件中配置 AI 服务:
```env
# markitdown 服务 URL(可选)
MARKITDOWN_URL=http://localhost:8000/convert
# AI 服务配置(用于智能字段提取)
AI_SERVICE_URL=your_ai_service_url
AI_API_KEY=your_api_key
```
## 📋 输出示例
解析后会生成:
- `docs/parsed/产品名称.json` - 解析结果
- `docs/parsed/产品名称.audit.md` - 审核报告
......@@ -15,6 +15,12 @@
*
* # 查看待处理文档
* npm run parse:docs -- --list
*
* # 应用审核通过的配置
* npm run parse:docs -- --apply=计划书模版4
*
* # 预览应用配置(不实际修改)
* npm run parse:docs -- --apply=计划书模版4 --dry-run
*/
import crypto from 'crypto'
import fs from 'fs'
......@@ -976,17 +982,27 @@ ${code.trim()}
## 📋 审核后操作
### 确认无误
### 方法 1:自动应用(推荐)
\`\`\`bash
# 预览变更(不实际修改)
pnpm parse:docs -- --apply=${baseFileName} --dry-run
# 确认无误后,正式应用
pnpm parse:docs -- --apply=${baseFileName}
# 说明:
# 1. 自动提取配置代码并插入到 src/config/plan-templates.js
# 2. 自动创建备份文件(docs/parsed-backup/)
# 3. 自动将审核文件移动到 docs/parse-audit/approved/
\`\`\`
### 方法 2:手动操作
\`\`\`bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/${baseFileName}/${auditFileName}
# 2. 手动复制"生成配置片段"到 src/config/plan-templates.js
\`\`\`
### 需要修改
......@@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) {
return true
}
/**
* 从审核文件应用配置到 plan-templates.js
*
* @description 读取审核 markdown 文件,提取配置代码,插入到配置文件中
* @param {string} auditFileName - 审核文件名(不含路径,如 "计划书模版4")
* @param {Object} options - 选项
* @param {boolean} options.dry_run - 是否仅预览
* @returns {Object} 应用结果
*/
function applyAuditFile(auditFileName, options = {}) {
const PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending')
const APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved')
// 1. 查找审核文件
let auditFile = null
let sourceDir = null
// 先在 pending 目录查找
const pendingDirs = fs.existsSync(PENDING_DIR) ? fs.readdirSync(PENDING_DIR) : []
for (const dir of pendingDirs) {
const dirPath = path.join(PENDING_DIR, dir)
if (fs.statSync(dirPath).isDirectory()) {
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'))
for (const file of files) {
// 匹配文件名或目录名
const normalizedName = dir.replace(/\s+/g, '').toLowerCase()
const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
auditFile = path.join(dirPath, file)
sourceDir = PENDING_DIR
break
}
}
}
if (auditFile) break
}
// 如果 pending 没找到,在 approved 目录查找
if (!auditFile && fs.existsSync(APPROVED_DIR)) {
const approvedFiles = fs.readdirSync(APPROVED_DIR).filter(f => f.endsWith('.md'))
for (const file of approvedFiles) {
// 从文件名提取产品名(格式:YYYY-MM-DD-产品名.md)
const match = file.match(/^\d{4}-\d{2}-\d{2}-(.+)\.md$/)
if (match) {
const normalizedName = match[1].replace(/\s+/g, '').toLowerCase()
const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
auditFile = path.join(APPROVED_DIR, file)
sourceDir = APPROVED_DIR
break
}
}
}
}
if (!auditFile) {
console.error("❌ 找不到审核文件: " + auditFileName)
console.log(" 搜索目录:")
console.log(" - docs/parse-audit/pending/")
console.log(" - docs/parse-audit/approved/")
return { ok: false, reason: 'file_not_found' }
}
console.log("\n📄 找到审核文件: " + auditFile)
// 2. 读取审核文件内容
const content = fs.readFileSync(auditFile, 'utf-8')
// 3. 提取配置代码片段
const configMatch = content.match(/## 🧩 生成配置片段\s*\n+```javascript\s*\n([\s\S]*?)```/)
if (!configMatch) {
console.error("❌ 无法从审核文件中提取配置代码")
return { ok: false, reason: 'config_not_found' }
}
const configCode = configMatch[1].trim()
console.log("\n📝 提取的配置代码:")
console.log("-".repeat(40))
console.log(configCode)
console.log("-".repeat(40))
// 4. 提取 form_sn 用于去重检查
const formSnMatch = configCode.match(/'([^']+)':\s*\{/)
const formSn = formSnMatch ? formSnMatch[1] : null
if (!formSn) {
console.error("❌ 无法从配置代码中提取 form_sn")
return { ok: false, reason: 'form_sn_not_found' }
}
console.log("\n🔑 form_sn: " + formSn)
// 5. 读取现有配置文件
const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')
// 检查是否已存在
if (existingContent.includes(`'${formSn}':`)) {
console.error("❌ 配置文件中已存在 form_sn: " + formSn)
console.log(" 如需更新,请先手动删除旧配置")
return { ok: false, reason: 'duplicate', formSn }
}
// 6. 找到插入位置(PLAN_TEMPLATES 对象的结束位置)
// 查找最后一个产品配置的结束位置
const insertPattern = /(\n\s*'\w+[^']+':\s*\{[\s\S]*?\n\s*\}\s*,?\s*)(\n\})/
const match = existingContent.match(insertPattern)
if (!match) {
console.error("❌ 无法定位插入位置")
return { ok: false, reason: 'insert_not_found' }
}
// 7. 构建新配置(确保有逗号)
let newConfigEntry = configCode
// 确保配置以逗号结尾
if (!newConfigEntry.trimEnd().endsWith(',')) {
newConfigEntry = newConfigEntry.trimEnd() + ','
}
// 8. 插入配置
const insertPosition = match.index + match[1].length
const updatedContent =
existingContent.slice(0, insertPosition) +
'\n\n' +
newConfigEntry +
existingContent.slice(insertPosition)
if (options.dry_run) {
console.log("\n🧪 dry-run 模式,变更预览:")
console.log("-".repeat(40))
console.log("将插入以下配置:")
console.log(newConfigEntry)
console.log("-".repeat(40))
return { ok: true, dry_run: true, formSn }
}
// 9. 备份并写入
let backupFile = null
if (fs.existsSync(CONFIG_FILE)) {
ensureDir(BACKUP_DIR)
backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
fs.copyFileSync(CONFIG_FILE, backupFile)
console.log("\n💾 已备份到: " + backupFile)
}
writeFile(CONFIG_FILE, updatedContent)
console.log("\n✅ 配置已更新: " + CONFIG_FILE)
writeBackupLog({
action: 'apply_audit',
backup_file: backupFile,
target_file: CONFIG_FILE,
audit_file: auditFile,
form_sn: formSn,
at: new Date().toISOString()
})
// 10. 移动审核文件到 approved 目录(如果是从 pending 来的)
if (sourceDir === PENDING_DIR) {
ensureDir(APPROVED_DIR)
const fileName = path.basename(auditFile)
const approvedPath = path.join(APPROVED_DIR, fileName)
// 检查目标是否已存在
if (fs.existsSync(approvedPath)) {
console.log("⚠️ approved 目录已存在同名文件,跳过移动")
} else {
fs.renameSync(auditFile, approvedPath)
console.log("📁 审核文件已移动到: " + approvedPath)
// 删除空的 pending 子目录
const pendingSubDir = path.dirname(auditFile)
const remainingFiles = fs.readdirSync(pendingSubDir).filter(f => !f.startsWith('.'))
if (remainingFiles.length === 0) {
fs.rmdirSync(pendingSubDir)
console.log("🗑️ 已删除空目录: " + pendingSubDir)
}
}
}
return { ok: true, formSn, backupFile }
}
function updateConfigFile(newConfigs, options = {}) {
console.log("\n" + "=".repeat(60))
console.log("📝 更新配置文件: " + CONFIG_FILE)
......@@ -1563,9 +1762,16 @@ async function main() {
const listMode = args.includes('--list')
const fileMode = args.find(arg => arg.startsWith('--file='))
const writeMode = args.includes('--write-config')
const dryRunMode = args.includes('--dry-run') || !writeMode
const rollbackMode = args.find(arg => arg.startsWith('--rollback='))
const statusMode = args.includes('--status')
const applyMode = args.find(arg => arg.startsWith('--apply='))
// dry-run 逻辑:
// 1. 如果显式指定 --dry-run,则 dry-run
// 2. 如果是 apply 模式,默认不 dry-run(除非显式指定)
// 3. 如果是解析模式,默认 dry-run(除非显式指定 --write-config)
const explicitDryRun = args.includes('--dry-run')
const dryRunMode = applyMode ? explicitDryRun : (!writeMode && !explicitDryRun || explicitDryRun)
// 检查解析器选择
const parserModeArg = args.find(arg => arg.startsWith('--parser='))
......@@ -1586,6 +1792,11 @@ async function main() {
if (rollbackMode) {
const backupFile = rollbackMode.split('=')[1]
rollbackConfigFile(backupFile)
} else if (applyMode) {
// 从审核文件应用配置
const auditFileName = applyMode.split('=')[1]
const applyOptions = { dry_run: dryRunMode }
applyAuditFile(auditFileName, applyOptions)
} else if (listMode) {
// 列出模式
const docs = getDocsToParse()
......
......@@ -16,6 +16,7 @@
* - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
* - FA 宏浚傳承保障計劃
* - LV2 赤霞珠終身壽險計劃2基本人壽保障選項
* - LV3 长宁終身壽險計劃3
*/
const PRODUCT_TITLE_PATTERNS = [
// 产品代码 + 产品名称 + 可选后缀
......@@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [
/^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm,
// 产品代码开头的行
/^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm
/^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm,
// 新增:产品代码 + 产品名称 + 数字后缀(如 "LV3 长宁終身壽險計劃3")
/^([A-Z]{2,3}\d?)\s+([^\n]{2,25}?(?:計劃|计划|壽險|壽险)\d?)/gm
]
/**
* 产品代码前缀列表(用于优先匹配)
*/
const PRODUCT_CODE_PREFIXES = [
'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR',
'GS', 'GC', 'FA', 'LV2', 'LV3', 'LV', 'CR', 'HR', 'PR', 'SR',
'TR', 'UR', 'WR', 'XR', 'YR', 'ZR'
]
......@@ -62,10 +66,11 @@ export function detectProductCount(content) {
export function findProductTitles(content) {
const products = []
const seenCodes = new Set()
const seenNames = new Set()
// 策略1: 优先匹配产品代码前缀
for (const prefix of PRODUCT_CODE_PREFIXES) {
// 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃"
// 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 或 "LV3 长宁終身壽險計劃3"
const regex = new RegExp(
`^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`,
'gm'
......@@ -76,9 +81,11 @@ export function findProductTitles(content) {
const code = match[1]
const name = match[2].trim()
// 去重
if (seenCodes.has(code)) continue
// 去重(基于代码或名称)
const nameKey = name.replace(/\s+/g, '').toLowerCase()
if (seenCodes.has(code) || seenNames.has(nameKey)) continue
seenCodes.add(code)
seenNames.add(nameKey)
products.push({
index: match.index,
......@@ -99,15 +106,52 @@ export function findProductTitles(content) {
const fullTitle = match[0].trim()
if (fullTitle.length < 5) continue // 过滤太短的匹配
const code = match[1] || null
const name = match[2] || fullTitle
// 去重
const nameKey = name.replace(/\s+/g, '').toLowerCase()
if (seenNames.has(nameKey)) continue
if (code) seenCodes.add(code)
seenNames.add(nameKey)
products.push({
index: match.index,
code: match[1] || null,
name: match[2] || fullTitle,
code,
name,
fullTitle
})
}
}
// 策略3: 新增 - 识别包含"计划"但不包含产品代码的行(纯计划书名称)
// 适用于标题如 "宏挚传承保障计划" 或 "长宁终身寿险计划3"
if (products.length === 0) {
const planNameRegex = /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/gm
let match
while ((match = planNameRegex.exec(content)) !== null) {
const fullTitle = match[1].trim()
// 排除太短或包含其他关键词的行
if (fullTitle.length < 5 || fullTitle.includes('選項') || fullTitle.includes('选项')) continue
// 检查是否是产品名称(通常包含"保障"、"保险"、"寿险"等关键词)
if (/(?:保障|保险|壽險|壽险|传承|家传)/.test(fullTitle)) {
const nameKey = fullTitle.replace(/\s+/g, '').toLowerCase()
if (!seenNames.has(nameKey)) {
seenNames.add(nameKey)
products.push({
index: match.index,
code: null,
name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明
fullTitle
})
}
}
}
}
// 按出现位置排序
products.sort((a, b) => a.index - b.index)
......
# Scripts 目录整理报告
> 整理日期: 2026-02-15
## ✅ 整理完成
### 新目录结构
```
scripts/
├── README.md # 主说明文档
├── CLAUDE.md # Claude Code 指南(待处理)
├── api-generator/ # API 代码生成工具
│ ├── README.md # 使用说明
│ ├── GUIDE.md # 详细指南
│ ├── generateApiFromOpenAPI.js # 主脚本 ⭐
│ ├── apiDiff.js # API 对比工具
│ └── test-generate.js # 测试脚本
├── doc-parser/ # 文档解析工具
│ ├── README.md # 使用说明
│ ├── QUICKSTART.md # 快速开始
│ ├── .env.example # 环境变量示例
│ ├── parse-docs.js # 主脚本 ⭐
│ ├── parse-config.js # 配置文件
│ ├── smart-field-extractor.js # 字段提取器
│ ├── product-splitter.js # 产品分割器
│ └── parse-docs.test.js # 测试文件
└── changelog/ # CHANGELOG 管理工具
├── README.md # 使用说明
├── check-changelog.sh # 漏记检查 ⭐
└── archive-changelog.sh # 归档脚本
```
### npm scripts 已更新
| 命令 | 新路径 |
|------|--------|
| `pnpm api:generate` | `scripts/api-generator/generateApiFromOpenAPI.js` |
| `pnpm parse:docs` | `scripts/doc-parser/parse-docs.js` |
| `pnpm changelog:check` | `scripts/changelog/check-changelog.sh` |
---
## ⚠️ 未使用文件列表
以下文件**没有在 package.json 或其他脚本中直接调用**,请判断是否需要保留:
### 1. `scripts/changelog/archive-changelog.sh`
**状态**: 🟡 未直接引用
**功能**: CHANGELOG 归档脚本,当记录超过 20 条时自动归档
**使用方式**: 手动运行 `./scripts/changelog/archive-changelog.sh`
**建议**:
- [ ] 保留 - 作为手动维护工具
- [ ] 删除 - 不再需要归档功能
---
### 2. `scripts/api-generator/test-generate.js`
**状态**: 🟡 未直接引用
**功能**: 测试生成的 API 文件是否正确
**使用方式**: 手动运行 `node scripts/api-generator/test-generate.js`
**建议**:
- [ ] 保留 - 作为开发调试工具
- [ ] 删除 - 已有其他测试方式
---
### 3. `scripts/CLAUDE.md`
**状态**: 🟡 需要评估
**功能**: 给 Claude Code 的说明文档
**位置**: 当前在 scripts 根目录
**建议**:
- [ ] 保留在当前位置 - 作为 Claude 指南
- [ ] 移动到 doc-parser 目录 - 因为主要与文档解析相关
- [ ] 删除 - 内容已整合到其他文档
---
## 📝 整理后的改进
1. **✅ 功能分组清晰** - 按用途分为 3 个子目录
2. **✅ 每个子目录有 README** - 包含使用说明和文件介绍
3. **✅ 路径引用已更新** - package.json 已同步更新
4. **✅ 单个文件已收录** - 所有脚本都在对应的功能目录中
---
## 🔧 后续操作建议
1. **确认未使用文件** - 对上述 3 个文件做出保留/删除决定
2. **更新 CLAUDE.md** - 如果项目根目录的 CLAUDE.md 已包含相关内容,可以删除 scripts/CLAUDE.md
3. **测试脚本** - 运行 `pnpm api:generate``pnpm parse:docs` 确保路径更新正确
......@@ -5,10 +5,16 @@
* @module composables/useFieldDependencies
* @author Claude Code
* @created 2026-02-14
* @updated 2026-02-15 - 集成新的条件评估引擎,支持复杂条件
*/
import { computed, reactive, isRef } from 'vue'
import { computed, reactive, isRef, watch } from 'vue'
import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
import {
evaluateCondition,
getConditionDependencies,
convertToNewFormat
} from '@/config/plan-conditions'
/**
* �测循环依赖
......@@ -88,6 +94,7 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
/**
* 检查字段是否应该显示
*
* @description 使用新的条件评估引擎,支持复杂条件(AND/OR/NOT/嵌套)
* @param {string} fieldKey - 字段键名
* @returns {boolean} 是否显示
*/
......@@ -96,28 +103,16 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
const definition = definitions[fieldKey]
if (!definition) return false
// 检查是否有 show_when 条件
// 使用新的条件评估引擎评估 show_when
if (definition.show_when) {
const conditions = definition.show_when
if (Array.isArray(conditions)) {
for (const condition of conditions) {
if (!condition) continue
const currentValue = formData[condition.field]
if (currentValue !== condition.equals) {
return false
}
}
} else if (conditions && typeof conditions === 'object') {
for (const [depKey, expectedValue] of Object.entries(conditions)) {
const currentValue = formData[depKey]
if (currentValue !== expectedValue) {
return false
}
}
// 转换为新格式(向后兼容)
const normalizedCondition = convertToNewFormat(definition.show_when)
if (!evaluateCondition(normalizedCondition, formData)) {
return false
}
}
// 检查是否被依赖字段影响
// 检查是否被依赖字段影响(旧逻辑,保持兼容)
for (const [key, def] of Object.entries(definitions)) {
if (def.affects?.includes(fieldKey)) {
// 依赖字段必须为 true 才显示
......@@ -198,6 +193,88 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
// 初始化
initFieldStates()
/**
* 清理隐藏字段的值
*
* @description 当字段隐藏时,根据 clear_when_hidden 配置决定是否清空值
* @param {string} fieldKey - 字段键名
* @param {Object} definition - 字段定义
*/
function clearHiddenFieldValue(fieldKey, definition) {
// 默认行为:隐藏时不清空(保持向后兼容)
const clearConfig = definition.clear_when_hidden
// false 或 undefined:不清空
if (clearConfig === false || clearConfig === undefined) {
return
}
// true:清空为 undefined
if (clearConfig === true) {
formData[fieldKey] = undefined
return
}
// null:设置为 null
if (clearConfig === null) {
formData[fieldKey] = null
return
}
// 对象配置
if (typeof clearConfig === 'object') {
// 清空自身
if (clearConfig.clear_self !== false) {
formData[fieldKey] = undefined
}
// 级联清空依赖字段
if (Array.isArray(clearConfig.clear_dependents)) {
for (const depKey of clearConfig.clear_dependents) {
if (formData[depKey] !== undefined) {
formData[depKey] = undefined
}
}
}
}
}
/**
* 更新字段可见性并处理隐藏字段的清理
*
* @param {string} fieldKey - 字段键名
* @param {string} triggerFieldKey - 触发更新的字段键名(可选)
*/
function updateFieldVisibility(fieldKey, triggerFieldKey = null) {
const definitions = getFieldDefinitions()
const definition = definitions[fieldKey]
if (!definition) return
const wasVisible = fieldVisibility[fieldKey]
const isVisible = isFieldVisible(fieldKey)
fieldVisibility[fieldKey] = isVisible
// 如果从可见变为不可见,且配置了清理规则,则清理字段值
if (wasVisible && !isVisible) {
clearHiddenFieldValue(fieldKey, definition)
}
// 更新启用状态
fieldEnabled[fieldKey] = isFieldEnabled(fieldKey)
}
/**
* 批量更新所有字段的可见性
*
* @description 通常在表单值变化后调用,重新计算所有字段的可见性
*/
function refreshAllVisibility() {
const definitions = getFieldDefinitions()
for (const key of Object.keys(definitions)) {
updateFieldVisibility(key)
}
}
return {
// 状态
fieldVisibility,
......@@ -208,6 +285,86 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
isFieldVisible,
isFieldEnabled,
updateFieldValue,
initFieldStates
initFieldStates,
updateFieldVisibility,
refreshAllVisibility
}
}
/**
* 过滤隐藏字段(用于提交前)
*
* @description 过滤掉当前不可见的字段,避免提交脏数据
* @param {Object} formData - 完整的表单数据
* @param {string[]} visibleFields - 可见字段列表
* @param {Object} options - 配置选项
* @param {string[]} options.alwaysInclude - 始终包含的字段(即使不可见)
* @returns {Object} 过滤后的表单数据
*
* @example
* const filteredData = filterHiddenFields(formData, visibleFields.value)
* await submitAPI(filteredData)
*/
export function filterHiddenFields(formData, visibleFields, options = {}) {
const { alwaysInclude = [] } = options
const filtered = {}
for (const [key, value] of Object.entries(formData)) {
// 可见字段始终包含
if (visibleFields.includes(key)) {
filtered[key] = value
continue
}
// 配置为始终包含的字段
if (alwaysInclude.includes(key)) {
filtered[key] = value
}
// 其他不可见字段被过滤掉
}
return filtered
}
/**
* 获取字段的条件依赖关系(用于调试和可视化)
*
* @description 返回字段的条件依赖图
* @param {Object} fieldDefinitions - 字段定义
* @returns {Map<string, Set<string>>} 字段到依赖字段的映射
*/
export function getFieldDependencyGraph(fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
const graph = new Map()
for (const [key, definition] of Object.entries(fieldDefinitions)) {
const deps = new Set()
// 从 show_when 提取依赖
if (definition.show_when) {
const conditionDeps = getConditionDependencies(definition.show_when)
conditionDeps.forEach(d => deps.add(d))
}
// 从 affects 提取反向依赖
if (definition.affects) {
for (const affectedKey of definition.affects) {
if (!graph.has(affectedKey)) {
graph.set(affectedKey, new Set())
}
graph.get(affectedKey).add(key)
}
}
// 从 depends_on 提取
if (definition.depends_on) {
deps.add(definition.depends_on)
}
if (deps.size > 0) {
graph.set(key, deps)
}
}
return graph
}
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -72,6 +72,7 @@ const savingsSubmitMapping = {
}
// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口)
// @updated 2026-02-15 - 迁移到新的条件规则格式,使用 clear_when_hidden 替代 reset_map
const savingsFormSchema = {
// 基础字段:非提取计划部分
base_fields: [
......@@ -83,24 +84,25 @@ const savingsFormSchema = {
{ id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' }
],
// 提取计划字段:由 withdrawal_plan 开关控制
// 新格式说明:
// - show_when: { field: 'xxx', op: 'eq', value: 'yyy' } 新格式条件
// - show_when: { field: 'xxx', equals: 'yyy' } 旧格式(向后兼容)
// - clear_when_hidden: true 隐藏时自动清空字段值
withdrawal_fields: [
{ id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' },
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' },
{ id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ 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: '指定提取金额' }] },
{ id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ 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: '指定提取金额' }] },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
{ 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: '最高固定提取金额' }] }
],
// 提取模式切换时的清空逻辑,避免脏字段影响提交
reset_map: {
withdrawal_mode: {
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'],
'指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed']
}
}
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)', clear_when_hidden: true },
// 指定提取金额模式字段
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
// 最高固定提取金额模式字段
{ 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 },
{ 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 }
]
// reset_map 已被 clear_when_hidden 替代
// 当 withdrawal_mode 切换时,不可见的字段会自动清空
}
export const PLAN_TEMPLATES = {
......@@ -366,6 +368,24 @@ export const PLAN_TEMPLATES = {
submit_mapping: savingsSubmitMapping
}
},
/**
* 长宁終身壽險計劃3
* @added 2026-02-15T06:30:03.691Z
* @source docs/to-parse/计划书模版4.docx
*/
'life-insurance-3-d8fde07d': {
name: '长宁終身壽險計劃3',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: ["5年","12年","15年","20年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
form_schema: protectionFormSchema,
submit_mapping: baseSubmitMapping
}
}
}
/**
......
......@@ -415,9 +415,11 @@ export async function mockProductListAPI(params) {
const list = []
const startIndex = page * limit
// 🔧 测试商品:第一页第一位固定为储蓄产品(form_sn:savings-product-30b41aae)
// 🔧 测试商品:第一页前两位固定为测试产品
if (page === 0) {
const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1)
// 测试商品1: 储蓄产品
const testProduct1 = {
id: 'savings-2-148b3acd',
product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)',
......@@ -432,19 +434,38 @@ export async function mockProductListAPI(params) {
_test_note: 'form_sn:savings-2-148b3acd'
}
// 检查分类和关键词过滤
let shouldInclude = true
if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) {
shouldInclude = false
}
if (keyword && !testProduct1.product_name.includes(keyword)) {
shouldInclude = false
// 测试商品2: 人寿保险产品
const testProduct2 = {
id: 'life-insurance-3-d8fde07d',
product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)',
cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300',
recommend: 'hot',
form_sn: 'life-insurance-3-d8fde07d', // ✅ 关键字段:对应真实 API 的 form_sn
created_time: new Date().toISOString(),
categories: [testCategory], // ✅ 符合真实 API 结构:categories 是数组
tags: [{ id: '1', name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }],
// 测试标识(不影响业务逻辑)
_test: true,
_test_note: 'form_sn:life-insurance-3-d8fde07d'
}
if (shouldInclude) {
list.push(testProduct1)
console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd')
}
// 检查分类和关键词过滤,依次添加测试商品
const testProducts = [testProduct1, testProduct2]
testProducts.forEach((testProduct, index) => {
let shouldInclude = true
if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) {
shouldInclude = false
}
if (keyword && !testProduct.product_name.includes(keyword)) {
shouldInclude = false
}
if (shouldInclude) {
list.push(testProduct)
console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`)
}
})
}
for (let i = 0; i < limit; i++) {
......
......@@ -234,6 +234,16 @@ function resolveSchemaRefs(config) {
}
}
/**
* 构建表单 Schema 代码
*
* @description 将 schema 对象转换为代码字符串
* @param {Object|string} value - schema 值
* @param {string} fallbackRef - 回退引用名
* @returns {string} 代码字符串
*
* @updated 2026-02-15 - 支持新的条件格式 show_when: { field, op, value }
*/
function buildSchemaCode(value, fallbackRef) {
if (!value || isEmptyObject(value)) {
return fallbackRef
......@@ -241,13 +251,14 @@ function buildSchemaCode(value, fallbackRef) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const baseFields = value.base_fields
const withdrawalFields = value.withdrawal_fields
const resetMap = value.reset_map
const baseFieldsEmpty = Array.isArray(baseFields) && baseFields.length === 0
const withdrawalFieldsEmpty = !Array.isArray(withdrawalFields) || withdrawalFields.length === 0
const resetMapEmpty = !resetMap || (typeof resetMap === 'object' && !Array.isArray(resetMap) && Object.keys(resetMap).length === 0)
if (baseFieldsEmpty && withdrawalFieldsEmpty && resetMapEmpty) {
if (baseFieldsEmpty && withdrawalFieldsEmpty) {
return fallbackRef
}
// 处理字段中的 show_when 格式转换
const processedValue = processSchemaFields(value)
return JSON.stringify(processedValue, null, 2).replace(/\n/g, '\n ')
}
if (typeof value === 'string') {
return value
......@@ -255,6 +266,42 @@ function buildSchemaCode(value, fallbackRef) {
return JSON.stringify(value, null, 2).replace(/\n/g, '\n ')
}
/**
* 处理 Schema 字段,转换条件格式
*
* @private
*/
function processSchemaFields(schema) {
const result = {}
for (const [key, value] of Object.entries(schema)) {
if (key === 'base_fields' || key === 'withdrawal_fields') {
result[key] = value.map(field => processFieldCondition(field))
} else if (key !== 'reset_map') {
// 忽略 reset_map,不再生成
result[key] = value
}
}
return result
}
/**
* 处理字段条件,转换为新格式
*
* @private
*/
function processFieldCondition(field) {
const result = { ...field }
// 转换 show_when: { field, equals } -> { field, op: 'eq', value }
if (result.show_when && result.show_when.equals !== undefined) {
result.show_when = {
field: result.show_when.field,
op: 'eq',
value: result.show_when.equals
}
}
return result
}
function isEmptyObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
......