Showing
21 changed files
with
2437 additions
and
39 deletions
| ... | @@ -7,15 +7,21 @@ export {} | ... | @@ -7,15 +7,21 @@ export {} |
| 7 | 7 | ||
| 8 | declare module 'vue' { | 8 | declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | + AgePicker: typeof import('./src/components/PlanFields/AgePicker.vue')['default'] | ||
| 11 | + AmountInput: typeof import('./src/components/PlanFields/AmountInput.vue')['default'] | ||
| 12 | + CriticalIllnessTemplate: typeof import('./src/components/PlanTemplates/CriticalIllnessTemplate.vue')['default'] | ||
| 13 | + DatePicker: typeof import('./src/components/PlanFields/DatePicker.vue')['default'] | ||
| 10 | DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default'] | 14 | DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default'] |
| 11 | FilterTabs: typeof import('./src/components/FilterTabs.vue')['default'] | 15 | FilterTabs: typeof import('./src/components/FilterTabs.vue')['default'] |
| 12 | 'FilterTabs.example': typeof import('./src/components/FilterTabs.example.vue')['default'] | 16 | 'FilterTabs.example': typeof import('./src/components/FilterTabs.example.vue')['default'] |
| 13 | IconFont: typeof import('./src/components/IconFont.vue')['default'] | 17 | IconFont: typeof import('./src/components/IconFont.vue')['default'] |
| 14 | IndexNav: typeof import('./src/components/indexNav.vue')['default'] | 18 | IndexNav: typeof import('./src/components/indexNav.vue')['default'] |
| 19 | + LifeInsuranceTemplate: typeof import('./src/components/PlanTemplates/LifeInsuranceTemplate.vue')['default'] | ||
| 15 | ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] | 20 | ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] |
| 16 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] | 21 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] |
| 17 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] | 22 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] |
| 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 23 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 24 | + NutDatepicker: typeof import('@nutui/nutui-taro')['Datepicker'] | ||
| 19 | NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] | 25 | NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] |
| 20 | NutInput: typeof import('@nutui/nutui-taro')['Input'] | 26 | NutInput: typeof import('@nutui/nutui-taro')['Input'] |
| 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] | 27 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] |
| ... | @@ -27,17 +33,21 @@ declare module 'vue' { | ... | @@ -27,17 +33,21 @@ declare module 'vue' { |
| 27 | OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] | 33 | OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] |
| 28 | PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] | 34 | PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] |
| 29 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | 35 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] |
| 36 | + PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default'] | ||
| 30 | PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default'] | 37 | PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default'] |
| 31 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 38 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] |
| 32 | QrCode: typeof import('./src/components/qrCode.vue')['default'] | 39 | QrCode: typeof import('./src/components/qrCode.vue')['default'] |
| 33 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] | 40 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] |
| 41 | + RadioGroup: typeof import('./src/components/PlanFields/RadioGroup.vue')['default'] | ||
| 34 | RouterLink: typeof import('vue-router')['RouterLink'] | 42 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 35 | RouterView: typeof import('vue-router')['RouterView'] | 43 | RouterView: typeof import('vue-router')['RouterView'] |
| 44 | + SavingsTemplate: typeof import('./src/components/SavingsTemplate.vue')['default'] | ||
| 36 | SchemeA: typeof import('./src/components/PlanSchemes/SchemeA.vue')['default'] | 45 | SchemeA: typeof import('./src/components/PlanSchemes/SchemeA.vue')['default'] |
| 37 | SchemeB: typeof import('./src/components/PlanSchemes/SchemeB.vue')['default'] | 46 | SchemeB: typeof import('./src/components/PlanSchemes/SchemeB.vue')['default'] |
| 38 | SearchBar: typeof import('./src/components/SearchBar.vue')['default'] | 47 | SearchBar: typeof import('./src/components/SearchBar.vue')['default'] |
| 39 | SectionCard: typeof import('./src/components/SectionCard.vue')['default'] | 48 | SectionCard: typeof import('./src/components/SectionCard.vue')['default'] |
| 40 | SectionItem: typeof import('./src/components/SectionItem.vue')['default'] | 49 | SectionItem: typeof import('./src/components/SectionItem.vue')['default'] |
| 50 | + SelectPicker: typeof import('./src/components/PlanFields/SelectPicker.vue')['default'] | ||
| 41 | TabBar: typeof import('./src/components/TabBar.vue')['default'] | 51 | TabBar: typeof import('./src/components/TabBar.vue')['default'] |
| 42 | } | 52 | } |
| 43 | } | 53 | } | ... | ... |
docs/PLAN/plan-entry-architecture.md
0 → 100644
This diff is collapsed. Click to expand it.
docs/PLAN/计划书模版.md
0 → 100644
| 1 | +## 一、人寿产品 | ||
| 2 | + | ||
| 3 | +1. WIOP3E 盈传创富保障计划 3 - 优选版 | ||
| 4 | +2. WIOP3 - 盈传创富保障计划 3 | ||
| 5 | + | ||
| 6 | +### 核心信息 | ||
| 7 | + | ||
| 8 | +性别、年龄、出生年月日、是否吸烟 | ||
| 9 | + | ||
| 10 | +### 保额 | ||
| 11 | + | ||
| 12 | +### 缴费年期 | ||
| 13 | + | ||
| 14 | +1. 整付(0-75 岁) | ||
| 15 | +2. 5 年(0-70 岁) | ||
| 16 | +3. 10 年(0-70 岁) | ||
| 17 | + | ||
| 18 | +## 二、重疾产品 | ||
| 19 | + | ||
| 20 | +1. MPC 守护无间重疾 | ||
| 21 | +2. MBC PRO 活跃人生重疾保 PRO | ||
| 22 | +3. MBC2 活跃人生重疾保 2 | ||
| 23 | + | ||
| 24 | +### 核心信息 | ||
| 25 | + | ||
| 26 | +性别、年龄、出生年月日、是否吸烟 | ||
| 27 | + | ||
| 28 | +### 保额 | ||
| 29 | + | ||
| 30 | +### 缴费年期 | ||
| 31 | + | ||
| 32 | +1. 10 年(15 日 - 65 岁) | ||
| 33 | +2. 20 年(15 日 - 65 岁) | ||
| 34 | +3. 25 年(15 日 - 60 岁) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
docs/plan-test-implementation.md
0 → 100644
| 1 | +# 计划书功能测试实现记录 | ||
| 2 | + | ||
| 3 | +**日期**: 2026-02-06 | ||
| 4 | +**目的**: 前端测试计划书录入功能(后端接口尚未准备好) | ||
| 5 | + | ||
| 6 | +--- | ||
| 7 | + | ||
| 8 | +## 📋 已完成的任务 | ||
| 9 | + | ||
| 10 | +### ✅ 任务1: 添加计划书按钮 | ||
| 11 | + | ||
| 12 | +#### 1.1 首页热卖产品卡片 | ||
| 13 | +**文件**: `src/pages/index/index.vue` | ||
| 14 | + | ||
| 15 | +**修改内容**: | ||
| 16 | +- 替换了旧的 `PlanPopup` + `SchemeA/SchemeB` 系统 | ||
| 17 | +- 集成新的 `PlanFormContainer` 组件 | ||
| 18 | +- 添加了计划书弹窗状态管理 (`showPlanPopup`, `selectedProduct`) | ||
| 19 | +- 更新了 `openPlanPopup()` 函数,根据产品ID查找产品对象 | ||
| 20 | +- 更新了 `handlePlanSubmit()` 函数 | ||
| 21 | + | ||
| 22 | +**计划书按钮**: 已存在(第84-91行),无需新增 | ||
| 23 | + | ||
| 24 | +#### 1.2 产品中心列表卡片 | ||
| 25 | +**文件**: `src/pages/knowledge-base/index.vue` | ||
| 26 | + | ||
| 27 | +**修改内容**: | ||
| 28 | +- 在产品卡片上添加了"详情"和"计划书"两个按钮 | ||
| 29 | +- 集成 `PlanFormContainer` 组件 | ||
| 30 | +- 添加了计划书弹窗状态管理 | ||
| 31 | +- 添加了 `openPlanPopup(product)` 函数 | ||
| 32 | +- 添加了 `handlePlanSubmit(formData)` 函数 | ||
| 33 | + | ||
| 34 | +**按钮布局**: | ||
| 35 | +``` | ||
| 36 | +[详情] [计划书] | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +#### 1.3 产品详情页 | ||
| 40 | +**文件**: `src/pages/product-detail/index.vue` | ||
| 41 | + | ||
| 42 | +**修改内容**: | ||
| 43 | +- 在页面底部添加了固定定位的"制作计划书"按钮 | ||
| 44 | +- 集成 `PlanFormContainer` 组件 | ||
| 45 | +- 添加了计划书弹窗状态管理 | ||
| 46 | +- 添加了 `openPlanPopup()` 函数 | ||
| 47 | +- 添加了 `handlePlanSubmit(formData)` 函数 | ||
| 48 | + | ||
| 49 | +**按钮样式**: 全宽按钮,固定在页面底部 | ||
| 50 | + | ||
| 51 | +--- | ||
| 52 | + | ||
| 53 | +### ✅ 任务3: Mock 数据(热卖产品) | ||
| 54 | + | ||
| 55 | +**文件**: `src/pages/index/index.vue` | ||
| 56 | +**函数**: `fetchHotProducts()` | ||
| 57 | + | ||
| 58 | +**Mock 数据包含全部7种产品类型**: | ||
| 59 | + | ||
| 60 | +#### 人寿保险(2种) | ||
| 61 | +1. **WIOP3E 盈传创富保障计划 3 - 优选版** | ||
| 62 | + - form_sn: `life-insurance-wiop3e` | ||
| 63 | + - 标签: 终身寿险、美元 | ||
| 64 | + | ||
| 65 | +2. **WIOP3 - 盈传创富保障计划 3** | ||
| 66 | + - form_sn: `life-insurance-wiop3` | ||
| 67 | + - 标签: 终身寿险、美元 | ||
| 68 | + | ||
| 69 | +#### 重疾保险(3种) | ||
| 70 | +3. **MPC 守护无间重疾** | ||
| 71 | + - form_sn: `critical-illness-mpc` | ||
| 72 | + - 标签: 重疾保障、人民币 | ||
| 73 | + | ||
| 74 | +4. **MBC PRO 活跃人生重疾保 PRO** | ||
| 75 | + - form_sn: `critical-illness-mbc-pro` | ||
| 76 | + - 标签: 重疾保障、人民币 | ||
| 77 | + | ||
| 78 | +5. **MBC2 活跃人生重疾保 2** | ||
| 79 | + - form_sn: `critical-illness-mbc2` | ||
| 80 | + - 标签: 重疾保障、人民币 | ||
| 81 | + | ||
| 82 | +#### 储蓄型产品(4种) | ||
| 83 | +6. **GS - 宏摯傳承保障計劃** | ||
| 84 | + - form_sn: `savings-gs` | ||
| 85 | + - 标签: 储蓄分红、美元 | ||
| 86 | + | ||
| 87 | +7. **GC - 宏摯家傳承保險計劃** | ||
| 88 | + - form_sn: `savings-gc` | ||
| 89 | + - 标签: 储蓄分红、美元 | ||
| 90 | + | ||
| 91 | +8. **FA - 宏浚傳承保障計劃** | ||
| 92 | + - form_sn: `savings-fa` | ||
| 93 | + - 标签: 储蓄分红、美元 | ||
| 94 | + | ||
| 95 | +9. **LV2 - 赤霞珠終身壽險計劃2** | ||
| 96 | + - form_sn: `savings-lv2` | ||
| 97 | + - 标签: 储蓄型终身寿险、美元 | ||
| 98 | + | ||
| 99 | +--- | ||
| 100 | + | ||
| 101 | +## 🚫 待清理的测试数据 | ||
| 102 | + | ||
| 103 | +### ⚠️ 重要提醒 | ||
| 104 | + | ||
| 105 | +**测试完成后需要移除以下内容**: | ||
| 106 | + | ||
| 107 | +#### 1. 首页 Mock 数据 | ||
| 108 | +**文件**: `src/pages/index/index.vue` | ||
| 109 | +**位置**: `fetchHotProducts()` 函数 | ||
| 110 | + | ||
| 111 | +**清理步骤**: | ||
| 112 | +```javascript | ||
| 113 | +// 删除测试数据部分(⚠️ 测试数据开始 - 测试完成后需要移除 ⚠️) | ||
| 114 | +// 恢复真实API调用: | ||
| 115 | +const res = await listAPI({ | ||
| 116 | + recommend: 'hot' | ||
| 117 | +}); | ||
| 118 | + | ||
| 119 | +if (res.code === 1 && res.data && res.data.list) { | ||
| 120 | + hotProducts.value = res.data.list; | ||
| 121 | +} | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +#### 2. 提交 API 接口对接 | ||
| 125 | +**文件**: `src/pages/index/index.vue`, `src/pages/product-detail/index.vue`, `src/pages/knowledge-base/index.vue` | ||
| 126 | + | ||
| 127 | +**当前状态**: | ||
| 128 | +```javascript | ||
| 129 | +// TODO: 后端接口还没有准备好,暂时不调用API | ||
| 130 | +// 测试完成后需要对接 submitPlanAPI | ||
| 131 | +``` | ||
| 132 | + | ||
| 133 | +**对接步骤**: | ||
| 134 | +1. 导入 API: `import { submitPlanAPI } from '@/api/plan'` | ||
| 135 | +2. 替换 `handlePlanSubmit()` 中的 TODO 部分: | ||
| 136 | +```javascript | ||
| 137 | +const res = await submitPlanAPI({ | ||
| 138 | + product_id: selectedProduct.value.id, | ||
| 139 | + template: selectedProduct.value.form_sn, | ||
| 140 | + form_data: formData | ||
| 141 | +}); | ||
| 142 | + | ||
| 143 | +if (res.code === 1) { | ||
| 144 | + // 跳转到成功页面 | ||
| 145 | + go('/pages/plan-submit-result/index', { | ||
| 146 | + success: 'true', | ||
| 147 | + plan_id: res.data.plan_id | ||
| 148 | + }); | ||
| 149 | +} | ||
| 150 | +``` | ||
| 151 | + | ||
| 152 | +--- | ||
| 153 | + | ||
| 154 | +## 📊 测试覆盖范围 | ||
| 155 | + | ||
| 156 | +### 模版类型测试 | ||
| 157 | +- ✅ **LifeInsuranceTemplate**: WIOP3E, WIOP3 | ||
| 158 | +- ✅ **CriticalIllnessTemplate**: MPC, MBC PRO, MBC2 | ||
| 159 | +- ✅ **SavingsTemplate**: GS, GC, FA, LV2 | ||
| 160 | + | ||
| 161 | +### 功能测试 | ||
| 162 | +- ✅ 计划书按钮显示在多个页面 | ||
| 163 | +- ✅ 点击按钮打开对应产品的计划书表单 | ||
| 164 | +- ✅ 根据产品的 `form_sn` 自动加载正确的模版 | ||
| 165 | +- ✅ 表单数据结构和验证 | ||
| 166 | + | ||
| 167 | +### 页面集成测试 | ||
| 168 | +- ✅ 首页 → 热卖产品卡片 → 计划书按钮 | ||
| 169 | +- ✅ 产品中心 → 产品列表卡片 → 计划书按钮 | ||
| 170 | +- ✅ 产品详情页 → 制作计划书按钮 | ||
| 171 | + | ||
| 172 | +--- | ||
| 173 | + | ||
| 174 | +## 🔍 后端接口需求 | ||
| 175 | + | ||
| 176 | +### 必需字段 | ||
| 177 | + | ||
| 178 | +产品 API 需要返回以下字段: | ||
| 179 | + | ||
| 180 | +```javascript | ||
| 181 | +{ | ||
| 182 | + id: 1, | ||
| 183 | + product_name: "产品名称", | ||
| 184 | + form_sn: "life-insurance-wiop3e", // 关键:计划书模版标识 | ||
| 185 | + recommend: "hot", // 可选:推荐标识 | ||
| 186 | + tags: [ // 可选:产品标签 | ||
| 187 | + { | ||
| 188 | + id: 1, | ||
| 189 | + name: "终身寿险", | ||
| 190 | + bg_color: "#DBEAFE", | ||
| 191 | + text_color: "#1E40AF" | ||
| 192 | + } | ||
| 193 | + ] | ||
| 194 | +} | ||
| 195 | +``` | ||
| 196 | + | ||
| 197 | +### 提交计划书 API | ||
| 198 | + | ||
| 199 | +**端点**: `/srv/?a=submit_plan&t=submit` | ||
| 200 | +**方法**: POST | ||
| 201 | +**参数**: | ||
| 202 | +```javascript | ||
| 203 | +{ | ||
| 204 | + product_id: number, // 产品ID | ||
| 205 | + template: string, // form_sn | ||
| 206 | + form_data: { | ||
| 207 | + // 基础字段(所有模版) | ||
| 208 | + gender: string, // "男" | "女" | ||
| 209 | + age: number, // 年龄 | ||
| 210 | + birthday: string, // "YYYY-MM-DD" | ||
| 211 | + smoker: string, // "是" | "否" | ||
| 212 | + coverage: number, // 保额(分) | ||
| 213 | + payment_period: string, // 缴费年期 | ||
| 214 | + | ||
| 215 | + // 储蓄产品专用字段 | ||
| 216 | + withdrawal_plan: { | ||
| 217 | + mode: string, // "年龄指定金额" | "最高固定金额" | ||
| 218 | + start_age: number, // 开始年龄 | ||
| 219 | + withdrawal_period: string, // 提取年期 | ||
| 220 | + | ||
| 221 | + // mode = "年龄指定金额" 时的额外字段 | ||
| 222 | + annual_amount: number, // 每年提取金额(分) | ||
| 223 | + currency: string, // 币种 | ||
| 224 | + increase_rate: number // 增加率 | ||
| 225 | + } | ||
| 226 | + } | ||
| 227 | +} | ||
| 228 | +``` | ||
| 229 | + | ||
| 230 | +--- | ||
| 231 | + | ||
| 232 | +## 📝 清理清单 | ||
| 233 | + | ||
| 234 | +测试完成后,按以下顺序清理: | ||
| 235 | + | ||
| 236 | +- [ ] **步骤1**: 移除首页 Mock 数据,恢复真实 API | ||
| 237 | +- [ ] **步骤2**: 对接提交计划书 API | ||
| 238 | +- [ ] **步骤3**: 移除所有 `TODO: 后端接口还没有准备好` 的注释 | ||
| 239 | +- [ ] **步骤4**: 移除 `console.log('⚠️ 使用测试数据...')` 等调试日志 | ||
| 240 | +- [ ] **步骤5**: 测试完整的提交流程 | ||
| 241 | +- [ ] **步骤6**: 删除本测试文档(或归档) | ||
| 242 | + | ||
| 243 | +--- | ||
| 244 | + | ||
| 245 | +## 🧪 测试指南 | ||
| 246 | + | ||
| 247 | +### 手动测试步骤 | ||
| 248 | + | ||
| 249 | +1. **首页测试**: | ||
| 250 | + - 打开首页,查看"热卖产品"区域 | ||
| 251 | + - 应显示9个产品卡片(2人寿 + 3重疾 + 4储蓄) | ||
| 252 | + - 点击任意产品的"计划书"按钮 | ||
| 253 | + - 应打开对应的计划书表单模版 | ||
| 254 | + | ||
| 255 | +2. **产品中心测试**: | ||
| 256 | + - 进入"产品中心"页面 | ||
| 257 | + - 选择任意分类或搜索 | ||
| 258 | + - 点击产品卡片上的"计划书"按钮 | ||
| 259 | + - 应打开对应的计划书表单模版 | ||
| 260 | + | ||
| 261 | +3. **产品详情页测试**: | ||
| 262 | + - 进入任意产品详情页 | ||
| 263 | + - 点击底部"制作计划书"按钮 | ||
| 264 | + - 应打开对应的计划书表单模版 | ||
| 265 | + | ||
| 266 | +4. **表单测试**: | ||
| 267 | + - 人寿保险模版:填写性别、年龄、出生年月日等 | ||
| 268 | + - 重疾保险模版:填写基础字段 | ||
| 269 | + - 储蓄产品模版:填写基础字段 + 提取计划功能 | ||
| 270 | + | ||
| 271 | +--- | ||
| 272 | + | ||
| 273 | +**最后更新**: 2026-02-06 | ||
| 274 | +**维护者**: Claude Code |
src/api/plan.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书 API 接口 | ||
| 3 | + * | ||
| 4 | + * @description 计划书相关的 API 接口定义 | ||
| 5 | + * @module api/plan | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-06 | ||
| 8 | + */ | ||
| 9 | + | ||
| 10 | +import { fn } from './fn' | ||
| 11 | + | ||
| 12 | +const Api = { | ||
| 13 | + Submit: '/srv/?a=submit_plan&t=submit' | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 提交计划书 | ||
| 18 | + * | ||
| 19 | + * @description 提交计划书表单数据到后端,生成计划书 PDF | ||
| 20 | + * @param {Object} params - 请求参数 | ||
| 21 | + * @param {number} params.product_id - 产品 ID | ||
| 22 | + * @param {string} params.template - 模版标识(form_sn) | ||
| 23 | + * @param {Object} params.form_data - 表单数据 | ||
| 24 | + * @param {string} params.form_data.gender - 性别("男" | "女") | ||
| 25 | + * @param {number} params.form_data.age - 年龄 | ||
| 26 | + * @param {string} params.form_data.birthday - 出生日期(格式:YYYY-MM-DD) | ||
| 27 | + * @param {string} params.form_data.smoker - 是否吸烟("是" | "否") | ||
| 28 | + * @param {number} params.form_data.coverage - 保额(单位:分) | ||
| 29 | + * @param {string} params.form_data.payment_period - 缴费年期 | ||
| 30 | + * @returns {Promise<{ | ||
| 31 | + * code: number; // 状态码 | ||
| 32 | + * msg: string; // 消息 | ||
| 33 | + * data: { | ||
| 34 | + * plan_id: number; // 计划书 ID | ||
| 35 | + * status: string; // 状态:processing | generated | ||
| 36 | + * estimated_time: number; // 预计生成时间(秒) | ||
| 37 | + * }; | ||
| 38 | + * }>} | ||
| 39 | + * | ||
| 40 | + * @example | ||
| 41 | + * const res = await submitPlanAPI({ | ||
| 42 | + * product_id: 1, | ||
| 43 | + * template: 'life-insurance-wiop3e', | ||
| 44 | + * form_data: { | ||
| 45 | + * gender: '男', | ||
| 46 | + * age: 18, | ||
| 47 | + * birthday: '1990-01-01', | ||
| 48 | + * smoker: '否', | ||
| 49 | + * coverage: 100000, // 1000.00 元(单位:分) | ||
| 50 | + * payment_period: '10 年(0-70 岁)' | ||
| 51 | + * } | ||
| 52 | + * }) | ||
| 53 | + * | ||
| 54 | + * if (res.code === 1) { | ||
| 55 | + * console.log('计划书 ID:', res.data.plan_id) | ||
| 56 | + * console.log('预计生成时间:', res.data.estimated_time, '秒') | ||
| 57 | + * } | ||
| 58 | + */ | ||
| 59 | +export const submitPlanAPI = (params) => fn({ | ||
| 60 | + url: Api.Submit, | ||
| 61 | + method: 'POST', | ||
| 62 | + data: params | ||
| 63 | +}) | ||
| 64 | + | ||
| 65 | +// 注意:查询计划书状态功能暂不需要 | ||
| 66 | +// 计划书生成是半自动流程(小程序 → 公司 → 手动上传 → 用户查看) | ||
| 67 | +// 用户在"我的计划书"页面查看生成的计划书 |
| ... | @@ -13,7 +13,7 @@ const pages = [ | ... | @@ -13,7 +13,7 @@ const pages = [ |
| 13 | 'pages/document-demo/index', | 13 | 'pages/document-demo/index', |
| 14 | 'pages/onboarding/index', | 14 | 'pages/onboarding/index', |
| 15 | 'pages/family-office/index', | 15 | 'pages/family-office/index', |
| 16 | - 'pages/knowledge-base/index', | 16 | + 'pages/product-center/index', |
| 17 | 'pages/product-detail/index', | 17 | 'pages/product-detail/index', |
| 18 | 'pages/category-list/index', | 18 | 'pages/category-list/index', |
| 19 | 'pages/material-list/index', | 19 | 'pages/material-list/index', | ... | ... |
src/components/PlanFields/AgePicker.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 标签 --> | ||
| 4 | + <div v-if="label" class="text-sm text-gray-600 mb-2">{{ label }}</div> | ||
| 5 | + | ||
| 6 | + <!-- 触发区域 --> | ||
| 7 | + <div | ||
| 8 | + class="flex justify-between items-center border border-gray-200 rounded-lg p-3" | ||
| 9 | + :class="{ 'bg-gray-50': showPicker }" | ||
| 10 | + @tap="openPicker" | ||
| 11 | + > | ||
| 12 | + <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 13 | + {{ displayValue || placeholder }} | ||
| 14 | + </span> | ||
| 15 | + <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <!-- Picker 弹窗 --> | ||
| 19 | + <nut-popup | ||
| 20 | + position="bottom" | ||
| 21 | + v-model:visible="showPicker" | ||
| 22 | + :z-index="9999" | ||
| 23 | + :overlay="true" | ||
| 24 | + > | ||
| 25 | + <nut-picker | ||
| 26 | + :columns="ageColumns" | ||
| 27 | + :default-value="defaultValue" | ||
| 28 | + @confirm="onConfirm" | ||
| 29 | + @cancel="showPicker = false" | ||
| 30 | + /> | ||
| 31 | + </nut-popup> | ||
| 32 | + </div> | ||
| 33 | +</template> | ||
| 34 | + | ||
| 35 | +<script setup> | ||
| 36 | +/** | ||
| 37 | + * 年龄选择器组件 | ||
| 38 | + * | ||
| 39 | + * @description 使用 NutUI Popup + Picker 实现年龄选择 | ||
| 40 | + * - 显示格式:3位数字(如 018 表示 18 岁) | ||
| 41 | + * - 提交格式:数字(如 18) | ||
| 42 | + * - 年龄范围:0-120 岁 | ||
| 43 | + * @author Claude Code | ||
| 44 | + * @example | ||
| 45 | + * <AgePicker | ||
| 46 | + * v-model="age" | ||
| 47 | + * label="年龄" | ||
| 48 | + * placeholder="请选择年龄" | ||
| 49 | + * /> | ||
| 50 | + */ | ||
| 51 | +import { ref, computed } from 'vue' | ||
| 52 | +import IconFont from '@/components/IconFont.vue' | ||
| 53 | + | ||
| 54 | +/** | ||
| 55 | + * 组件属性 | ||
| 56 | + */ | ||
| 57 | +const props = defineProps({ | ||
| 58 | + /** | ||
| 59 | + * 标签文本 | ||
| 60 | + * @type {string} | ||
| 61 | + */ | ||
| 62 | + label: { | ||
| 63 | + type: String, | ||
| 64 | + default: '' | ||
| 65 | + }, | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * 占位符文本 | ||
| 69 | + * @type {string} | ||
| 70 | + */ | ||
| 71 | + placeholder: { | ||
| 72 | + type: String, | ||
| 73 | + default: '请选择年龄' | ||
| 74 | + }, | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * 绑定的值(数字) | ||
| 78 | + * @type {number} | ||
| 79 | + */ | ||
| 80 | + modelValue: { | ||
| 81 | + type: Number, | ||
| 82 | + default: null | ||
| 83 | + } | ||
| 84 | +}) | ||
| 85 | + | ||
| 86 | +/** | ||
| 87 | + * 组件事件 | ||
| 88 | + */ | ||
| 89 | +const emit = defineEmits([ | ||
| 90 | + /** | ||
| 91 | + * 更新值事件 | ||
| 92 | + * @event update:modelValue | ||
| 93 | + * @param {number} value - 选中的年龄(数字) | ||
| 94 | + */ | ||
| 95 | + 'update:modelValue' | ||
| 96 | +]) | ||
| 97 | + | ||
| 98 | +/** | ||
| 99 | + * 控制 Picker 显示 | ||
| 100 | + * @type {Ref<boolean>} | ||
| 101 | + */ | ||
| 102 | +const showPicker = ref(false) | ||
| 103 | + | ||
| 104 | +/** | ||
| 105 | + * 打开选择器 | ||
| 106 | + */ | ||
| 107 | +const openPicker = () => { | ||
| 108 | + showPicker.value = true | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +/** | ||
| 112 | + * 年龄选项(3位数字格式) | ||
| 113 | + * @description 生成 000-120 的年龄选项数组 | ||
| 114 | + * @returns {Array<Array<{text: string, value: number}>>} Picker 列格式 | ||
| 115 | + * | ||
| 116 | + * @example | ||
| 117 | + * // 返回值示例 | ||
| 118 | + * [ | ||
| 119 | + * [ | ||
| 120 | + * { text: '000', value: 0 }, | ||
| 121 | + * { text: '001', value: 1 }, | ||
| 122 | + * ... | ||
| 123 | + * { text: '018', value: 18 }, | ||
| 124 | + * ... | ||
| 125 | + * { text: '120', value: 120 } | ||
| 126 | + * ] | ||
| 127 | + * ] | ||
| 128 | + */ | ||
| 129 | +const ageColumns = computed(() => { | ||
| 130 | + const ages = [] | ||
| 131 | + for (let i = 0; i <= 120; i++) { | ||
| 132 | + // 0, 1, 2 -> '000', '001', '002' | ||
| 133 | + const ageStr = i.toString().padStart(3, '0') | ||
| 134 | + ages.push({ text: ageStr, value: i }) | ||
| 135 | + } | ||
| 136 | + return [ages] | ||
| 137 | +}) | ||
| 138 | + | ||
| 139 | +/** | ||
| 140 | + * 默认选中的值(3位数字格式) | ||
| 141 | + * @description 如果没有值,默认显示 018(18岁) | ||
| 142 | + * @returns {Array<string>} Picker 默认值格式 | ||
| 143 | + * | ||
| 144 | + * @example | ||
| 145 | + * // modelValue = 18 | ||
| 146 | + * defaultValue() // 返回: ['018'] | ||
| 147 | + * | ||
| 148 | + * // modelValue = null | ||
| 149 | + * defaultValue() // 返回: ['018'] | ||
| 150 | + */ | ||
| 151 | +const defaultValue = computed(() => { | ||
| 152 | + const age = props.modelValue || 18 | ||
| 153 | + return [age.toString().padStart(3, '0')] | ||
| 154 | +}) | ||
| 155 | + | ||
| 156 | +/** | ||
| 157 | + * 显示的值(数字格式) | ||
| 158 | + * @description 将数字转换为字符串显示 | ||
| 159 | + * @returns {string} 显示文本 | ||
| 160 | + * | ||
| 161 | + * @example | ||
| 162 | + * // modelValue = 18 | ||
| 163 | + * displayValue() // 返回: '18' | ||
| 164 | + * | ||
| 165 | + * // modelValue = null | ||
| 166 | + * displayValue() // 返回: '' | ||
| 167 | + */ | ||
| 168 | +const displayValue = computed(() => { | ||
| 169 | + return props.modelValue ? props.modelValue.toString() : '' | ||
| 170 | +}) | ||
| 171 | + | ||
| 172 | +/** | ||
| 173 | + * 确认选择 | ||
| 174 | + * @param {Object} params - Picker 返回参数 | ||
| 175 | + * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 176 | + * | ||
| 177 | + * @example | ||
| 178 | + * // 用户选择 018 | ||
| 179 | + * onConfirm({ selectedOptions: [{ text: '018', value: 18 }] }) | ||
| 180 | + * // -> emit('update:modelValue', 18) | ||
| 181 | + */ | ||
| 182 | +const onConfirm = ({ selectedOptions }) => { | ||
| 183 | + const age = selectedOptions[0]?.value | ||
| 184 | + if (age !== undefined) { | ||
| 185 | + emit('update:modelValue', age) | ||
| 186 | + } | ||
| 187 | + showPicker.value = false | ||
| 188 | +} | ||
| 189 | +</script> | ||
| 190 | + | ||
| 191 | +<style lang="less" scoped> | ||
| 192 | +/* 组件样式 */ | ||
| 193 | +</style> |
src/components/PlanFields/DatePicker.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 标签 --> | ||
| 4 | + <div v-if="label" class="text-sm text-gray-600 mb-2">{{ label }}</div> | ||
| 5 | + | ||
| 6 | + <!-- 触发区域 --> | ||
| 7 | + <div | ||
| 8 | + class="flex justify-between items-center border border-gray-200 rounded-lg p-3" | ||
| 9 | + :class="{ 'bg-gray-50': showDatePicker }" | ||
| 10 | + @tap="openDatePicker" | ||
| 11 | + > | ||
| 12 | + <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 13 | + {{ displayValue || placeholder }} | ||
| 14 | + </span> | ||
| 15 | + <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <!-- DatePicker 弹窗 --> | ||
| 19 | + <nut-datepicker | ||
| 20 | + v-model="showDatePicker" | ||
| 21 | + :min-date="minDate" | ||
| 22 | + :max-date="maxDate" | ||
| 23 | + @confirm="onConfirm" | ||
| 24 | + > | ||
| 25 | + </nut-datepicker> | ||
| 26 | + </div> | ||
| 27 | +</template> | ||
| 28 | + | ||
| 29 | +<script setup> | ||
| 30 | +/** | ||
| 31 | + * 日期选择器组件 | ||
| 32 | + * | ||
| 33 | + * @description 使用 NutUI DatePicker 实现日期选择 | ||
| 34 | + * - 支持年龄范围限制(minAge, maxAge) | ||
| 35 | + * - 格式:YYYY-MM-DD | ||
| 36 | + * - 可触发自动计算年龄 | ||
| 37 | + * @author Claude Code | ||
| 38 | + * @example | ||
| 39 | + * <DatePicker | ||
| 40 | + * v-model="birthday" | ||
| 41 | + * label="出生年月日" | ||
| 42 | + * placeholder="请选择日期" | ||
| 43 | + * :min-age="0" | ||
| 44 | + * :max-age="120" | ||
| 45 | + * @change="onBirthdayChange" | ||
| 46 | + * /> | ||
| 47 | + */ | ||
| 48 | +import { ref, computed } from 'vue' | ||
| 49 | +import IconFont from '@/components/IconFont.vue' | ||
| 50 | + | ||
| 51 | +/** | ||
| 52 | + * 组件属性 | ||
| 53 | + */ | ||
| 54 | +const props = defineProps({ | ||
| 55 | + /** | ||
| 56 | + * 标签文本 | ||
| 57 | + * @type {string} | ||
| 58 | + */ | ||
| 59 | + label: { | ||
| 60 | + type: String, | ||
| 61 | + default: '' | ||
| 62 | + }, | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 占位符文本 | ||
| 66 | + * @type {string} | ||
| 67 | + */ | ||
| 68 | + placeholder: { | ||
| 69 | + type: String, | ||
| 70 | + default: '请选择日期' | ||
| 71 | + }, | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 绑定的值(格式:YYYY-MM-DD) | ||
| 75 | + * @type {string} | ||
| 76 | + */ | ||
| 77 | + modelValue: { | ||
| 78 | + type: String, | ||
| 79 | + default: '' | ||
| 80 | + }, | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 最小年龄(用于计算最大出生日期) | ||
| 84 | + * @type {number} | ||
| 85 | + * @default 0 | ||
| 86 | + */ | ||
| 87 | + minAge: { | ||
| 88 | + type: Number, | ||
| 89 | + default: 0 | ||
| 90 | + }, | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 最大年龄(用于计算最小出生日期) | ||
| 94 | + * @type {number} | ||
| 95 | + * @default 120 | ||
| 96 | + */ | ||
| 97 | + maxAge: { | ||
| 98 | + type: Number, | ||
| 99 | + default: 120 } | ||
| 100 | +}) | ||
| 101 | + | ||
| 102 | +/** | ||
| 103 | + * 组件事件 | ||
| 104 | + */ | ||
| 105 | +const emit = defineEmits([ | ||
| 106 | + /** | ||
| 107 | + * 更新值事件 | ||
| 108 | + * @event update:modelValue | ||
| 109 | + * @param {string} value - 选中的日期(格式:YYYY-MM-DD) | ||
| 110 | + */ | ||
| 111 | + 'update:modelValue', | ||
| 112 | + | ||
| 113 | + /** | ||
| 114 | + * 值变化事件(可用于触发自动计算年龄) | ||
| 115 | + * @event change | ||
| 116 | + * @param {string} value - 选中的日期(格式:YYYY-MM-DD) | ||
| 117 | + */ | ||
| 118 | + 'change' | ||
| 119 | +]) | ||
| 120 | + | ||
| 121 | +/** | ||
| 122 | + * 控制 DatePicker 显示 | ||
| 123 | + * @type {Ref<boolean>} | ||
| 124 | + */ | ||
| 125 | +const showDatePicker = ref(false) | ||
| 126 | + | ||
| 127 | +/** | ||
| 128 | + * 打开日期选择器 | ||
| 129 | + */ | ||
| 130 | +const openDatePicker = () => { | ||
| 131 | + showDatePicker.value = true | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +/** | ||
| 135 | + * 计算最小可选日期(基于最大年龄) | ||
| 136 | + * @description maxAge 岁对应的出生日期 | ||
| 137 | + * @type {ComputedRef<Date>} | ||
| 138 | + * @example | ||
| 139 | + * // maxAge = 75, 当前日期 = 2026-02-06 | ||
| 140 | + * // minDate() // 返回: 1951-02-06 | ||
| 141 | + */ | ||
| 142 | +const minDate = computed(() => { | ||
| 143 | + const date = new Date() | ||
| 144 | + date.setFullYear(date.getFullYear() - props.maxAge) | ||
| 145 | + return date | ||
| 146 | +}) | ||
| 147 | + | ||
| 148 | +/** | ||
| 149 | + * 计算最大可选日期(基于最小年龄) | ||
| 150 | + * @description minAge 岁对应的出生日期 | ||
| 151 | + * @type {ComputedRef<Date>} | ||
| 152 | + * @example | ||
| 153 | + * // minAge = 0, 当前日期 = 2026-02-06 | ||
| 154 | + * // maxDate() // 返回: 2026-02-06 | ||
| 155 | + */ | ||
| 156 | +const maxDate = computed(() => { | ||
| 157 | + const date = new Date() | ||
| 158 | + date.setFullYear(date.getFullYear() - props.minAge) | ||
| 159 | + return date | ||
| 160 | +}) | ||
| 161 | + | ||
| 162 | +/** | ||
| 163 | + * 显示的值 | ||
| 164 | + * @type {ComputedRef<string>} | ||
| 165 | + */ | ||
| 166 | +const displayValue = computed(() => { | ||
| 167 | + return props.modelValue || '' | ||
| 168 | +}) | ||
| 169 | + | ||
| 170 | +/** | ||
| 171 | + * 确认选择 | ||
| 172 | + * @param {Object} values - DatePicker 返回的日期对象 | ||
| 173 | + * | ||
| 174 | + * @example | ||
| 175 | + * // 用户选择 2020-01-01 | ||
| 176 | + * onConfirm(new Date('2020-01-01')) | ||
| 177 | + * // -> emit('update:modelValue', '2020-01-01') | ||
| 178 | + * // -> emit('change', '2020-01-01') | ||
| 179 | + */ | ||
| 180 | +const onConfirm = (values) => { | ||
| 181 | + const date = values | ||
| 182 | + const year = date.getFullYear() | ||
| 183 | + const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| 184 | + const day = String(date.getDate()).padStart(2, '0') | ||
| 185 | + | ||
| 186 | + const formattedDate = `${year}-${month}-${day}` | ||
| 187 | + emit('update:modelValue', formattedDate) | ||
| 188 | + emit('change', formattedDate) | ||
| 189 | + showDatePicker.value = false | ||
| 190 | +} | ||
| 191 | +</script> | ||
| 192 | + | ||
| 193 | +<style lang="less" scoped> | ||
| 194 | +/* 组件样式 */ | ||
| 195 | +</style> |
src/components/PlanFields/RadioGroup.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 标签 --> | ||
| 4 | + <div v-if="label" class="text-sm text-gray-600 mb-2">{{ label }}</div> | ||
| 5 | + | ||
| 6 | + <!-- Radio Group --> | ||
| 7 | + <nut-radio-group v-model="selectedValue" direction="horizontal" class="mb-4"> | ||
| 8 | + <nut-radio | ||
| 9 | + v-for="option in options" | ||
| 10 | + :key="option" | ||
| 11 | + :label="option" | ||
| 12 | + class="mr-8" | ||
| 13 | + > | ||
| 14 | + {{ option }} | ||
| 15 | + </nut-radio> | ||
| 16 | + </nut-radio-group> | ||
| 17 | + </div> | ||
| 18 | +</template> | ||
| 19 | + | ||
| 20 | +<script setup> | ||
| 21 | +/** | ||
| 22 | + * 单选组组件 | ||
| 23 | + * | ||
| 24 | + * @description 使用 NutUI RadioGroup 实现单选功能 | ||
| 25 | + * - 支持 v-model 双向绑定 | ||
| 26 | + * - 横向排列 | ||
| 27 | + * @author Claude Code | ||
| 28 | + * @example | ||
| 29 | + * <RadioGroup | ||
| 30 | + * v-model="gender" | ||
| 31 | + * label="性别" | ||
| 32 | + * :options="['男', '女']" | ||
| 33 | + * /> | ||
| 34 | + */ | ||
| 35 | +import { computed } from 'vue' | ||
| 36 | + | ||
| 37 | +/** | ||
| 38 | + * 组件属性 | ||
| 39 | + */ | ||
| 40 | +const props = defineProps({ | ||
| 41 | + /** | ||
| 42 | + * 标签文本 | ||
| 43 | + * @type {string} | ||
| 44 | + */ | ||
| 45 | + label: { | ||
| 46 | + type: String, | ||
| 47 | + default: '' | ||
| 48 | + }, | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 选项数组 | ||
| 52 | + * @type {Array<string>} | ||
| 53 | + * @example ['男', '女'] | ||
| 54 | + * @example ['是', '否'] | ||
| 55 | + */ | ||
| 56 | + options: { | ||
| 57 | + type: Array, | ||
| 58 | + required: true | ||
| 59 | + }, | ||
| 60 | + | ||
| 61 | + /** | ||
| 62 | + * 绑定的值 | ||
| 63 | + * @type {string} | ||
| 64 | + */ | ||
| 65 | + modelValue: { | ||
| 66 | + type: String, | ||
| 67 | + default: '' | ||
| 68 | + } | ||
| 69 | +}) | ||
| 70 | + | ||
| 71 | +/** | ||
| 72 | + * 组件事件 | ||
| 73 | + */ | ||
| 74 | +const emit = defineEmits([ | ||
| 75 | + /** | ||
| 76 | + * 更新值事件 | ||
| 77 | + * @event update:modelValue | ||
| 78 | + * @param {string} value - 选中的选项 | ||
| 79 | + */ | ||
| 80 | + 'update:modelValue' | ||
| 81 | +]) | ||
| 82 | + | ||
| 83 | +/** | ||
| 84 | + * 当前选中的值(用于 v-model) | ||
| 85 | + * @type {ComputedRef<string>} | ||
| 86 | + */ | ||
| 87 | +const selectedValue = computed({ | ||
| 88 | + get: () => props.modelValue, | ||
| 89 | + set: (val) => emit('update:modelValue', val) | ||
| 90 | +}) | ||
| 91 | +</script> | ||
| 92 | + | ||
| 93 | +<style lang="less" scoped> | ||
| 94 | +/* 组件样式 */ | ||
| 95 | +</style> |
src/components/PlanFields/SelectPicker.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 标签 --> | ||
| 4 | + <div v-if="label" class="text-sm text-gray-600 mb-2">{{ label }}</div> | ||
| 5 | + | ||
| 6 | + <!-- 触发区域 --> | ||
| 7 | + <div | ||
| 8 | + class="flex justify-between items-center border border-gray-200 rounded-lg p-3" | ||
| 9 | + :class="{ 'bg-gray-50': showPicker }" | ||
| 10 | + @tap="openPicker" | ||
| 11 | + > | ||
| 12 | + <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 13 | + {{ displayValue || placeholder }} | ||
| 14 | + </span> | ||
| 15 | + <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <!-- Picker 弹窗 --> | ||
| 19 | + <nut-popup | ||
| 20 | + position="bottom" | ||
| 21 | + v-model:visible="showPicker" | ||
| 22 | + :z-index="9999" | ||
| 23 | + :overlay="true" | ||
| 24 | + > | ||
| 25 | + <nut-picker | ||
| 26 | + :columns="pickerColumns" | ||
| 27 | + @confirm="onConfirm" | ||
| 28 | + @cancel="showPicker = false" | ||
| 29 | + /> | ||
| 30 | + </nut-popup> | ||
| 31 | + </div> | ||
| 32 | +</template> | ||
| 33 | + | ||
| 34 | +<script setup> | ||
| 35 | +/** | ||
| 36 | + * 下拉选择器组件 | ||
| 37 | + * | ||
| 38 | + * @description 使用 NutUI Picker 实现下拉选择功能 | ||
| 39 | + * - key 和 value 相同(如"整付(0-75 岁)") | ||
| 40 | + * - 适用于缴费年期等场景 | ||
| 41 | + * @author Claude Code | ||
| 42 | + * @example | ||
| 43 | + * <SelectPicker | ||
| 44 | + * v-model="paymentPeriod" | ||
| 45 | + * label="缴费年期" | ||
| 46 | + * placeholder="请选择缴费年期" | ||
| 47 | + * :options="['整付(0-75 岁)', '5 年(0-70 岁)']" | ||
| 48 | + * /> | ||
| 49 | + */ | ||
| 50 | +import { ref, computed } from 'vue' | ||
| 51 | +import IconFont from '@/components/IconFont.vue' | ||
| 52 | + | ||
| 53 | +/** | ||
| 54 | + * 组件属性 | ||
| 55 | + */ | ||
| 56 | +const props = defineProps({ | ||
| 57 | + /** | ||
| 58 | + * 标签文本 | ||
| 59 | + * @type {string} | ||
| 60 | + */ | ||
| 61 | + label: { | ||
| 62 | + type: String, | ||
| 63 | + default: '' | ||
| 64 | + }, | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 占位符文本 | ||
| 68 | + * @type {string} | ||
| 69 | + */ | ||
| 70 | + placeholder: { | ||
| 71 | + type: String, | ||
| 72 | + default: '请选择' | ||
| 73 | + }, | ||
| 74 | + | ||
| 75 | + /** | ||
| 76 | + * 绑定的值 | ||
| 77 | + * @type {string} | ||
| 78 | + */ | ||
| 79 | + modelValue: { | ||
| 80 | + type: String, | ||
| 81 | + default: '' | ||
| 82 | + }, | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 选项数组(key 和 value 相同) | ||
| 86 | + * @type {Array<string>} | ||
| 87 | + * @example ['整付(0-75 岁)', '5 年(0-70 岁)', '10 年(0-70 岁)'] | ||
| 88 | + */ | ||
| 89 | + options: { | ||
| 90 | + type: Array, | ||
| 91 | + required: true | ||
| 92 | + } | ||
| 93 | +}) | ||
| 94 | + | ||
| 95 | +/** | ||
| 96 | + * 组件事件 | ||
| 97 | + */ | ||
| 98 | +const emit = defineEmits([ | ||
| 99 | + /** | ||
| 100 | + * 更新值事件 | ||
| 101 | + * @event update:modelValue | ||
| 102 | + * @param {string} value - 选中的选项 | ||
| 103 | + */ | ||
| 104 | + 'update:modelValue' | ||
| 105 | +]) | ||
| 106 | + | ||
| 107 | +/** | ||
| 108 | + * 控制 Picker 显示 | ||
| 109 | + * @type {Ref<boolean>} | ||
| 110 | + */ | ||
| 111 | +const showPicker = ref(false) | ||
| 112 | + | ||
| 113 | +/** | ||
| 114 | + * 打开选择器 | ||
| 115 | + */ | ||
| 116 | +const openPicker = () => { | ||
| 117 | + showPicker.value = true | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +/** | ||
| 121 | + * 转换为 Picker 格式 | ||
| 122 | + * @description 将选项数组转换为 Picker 需要的格式 | ||
| 123 | + * @type {ComputedRef<Array<{text: string, value: string}>>} | ||
| 124 | + * @example | ||
| 125 | + * // options = ['整付(0-75 岁)', '5 年(0-70 岁)'] | ||
| 126 | + * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...] | ||
| 127 | + */ | ||
| 128 | +const pickerColumns = computed(() => { | ||
| 129 | + return props.options.map(option => ({ | ||
| 130 | + text: option, | ||
| 131 | + value: option // key 和 value 相同 | ||
| 132 | + })) | ||
| 133 | +}) | ||
| 134 | + | ||
| 135 | +/** | ||
| 136 | + * 显示的值 | ||
| 137 | + * @type {ComputedRef<string>} | ||
| 138 | + */ | ||
| 139 | +const displayValue = computed(() => { | ||
| 140 | + return props.modelValue || '' | ||
| 141 | +}) | ||
| 142 | + | ||
| 143 | +/** | ||
| 144 | + * 确认选择 | ||
| 145 | + * @param {Object} params - Picker 返回参数 | ||
| 146 | + * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 147 | + * | ||
| 148 | + * @example | ||
| 149 | + * // 用户选择 '整付(0-75 岁)' | ||
| 150 | + * onConfirm({ selectedOptions: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }] }) | ||
| 151 | + * // -> emit('update:modelValue', '整付(0-75 岁)') | ||
| 152 | + */ | ||
| 153 | +const onConfirm = ({ selectedOptions }) => { | ||
| 154 | + const value = selectedOptions[0]?.value | ||
| 155 | + if (value !== undefined) { | ||
| 156 | + emit('update:modelValue', value) | ||
| 157 | + } | ||
| 158 | + showPicker.value = false | ||
| 159 | +} | ||
| 160 | +</script> | ||
| 161 | + | ||
| 162 | +<style lang="less" scoped> | ||
| 163 | +/* 组件样式 */ | ||
| 164 | +</style> |
src/components/PlanFormContainer.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <!-- 使用 PlanPopup 容器组件 --> | ||
| 3 | + <PlanPopup :title="templateConfig?.name || '计划书'" @close="close" @submit="submit"> | ||
| 4 | + <!-- 动态加载模版组件 --> | ||
| 5 | + <component | ||
| 6 | + :is="currentTemplateComponent" | ||
| 7 | + v-model="formData" | ||
| 8 | + :config="templateConfig" | ||
| 9 | + v-if="currentTemplateComponent" | ||
| 10 | + /> | ||
| 11 | + | ||
| 12 | + <!-- 错误提示 --> | ||
| 13 | + <div v-else class="text-center text-gray-500 py-10"> | ||
| 14 | + <p>⚠️ 未找到对应的计划书模版</p> | ||
| 15 | + <p class="text-sm mt-2">form_sn: {{ product?.form_sn }}</p> | ||
| 16 | + </div> | ||
| 17 | + </PlanPopup> | ||
| 18 | +</template> | ||
| 19 | + | ||
| 20 | +<script setup> | ||
| 21 | +/** | ||
| 22 | + * 计划书表单容器 | ||
| 23 | + * | ||
| 24 | + * @description 根据产品的 form_sn 动态加载对应的计划书模版组件 | ||
| 25 | + * - 自动识别产品并加载模版 | ||
| 26 | + * - 支持后端 plan_config 动态配置 | ||
| 27 | + * - 统一的表单提交处理 | ||
| 28 | + * @author Claude Code | ||
| 29 | + * @example | ||
| 30 | + * <PlanFormContainer | ||
| 31 | + * v-model:visible="showPlanPopup" | ||
| 32 | + * :product="selectedProduct" | ||
| 33 | + * @close="handleClose" | ||
| 34 | + * @submit="handleSubmit" | ||
| 35 | + * /> | ||
| 36 | + */ | ||
| 37 | +import { ref, computed, watch } from 'vue' | ||
| 38 | +import PlanPopup from './PlanPopup/index.vue' | ||
| 39 | +import LifeInsuranceTemplate from './PlanTemplates/LifeInsuranceTemplate.vue' | ||
| 40 | +import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue' | ||
| 41 | +import SavingsTemplate from './SavingsTemplate.vue' | ||
| 42 | +import { PLAN_TEMPLATES } from '@/config/plan-templates' | ||
| 43 | + | ||
| 44 | +/** | ||
| 45 | + * 组件属性 | ||
| 46 | + */ | ||
| 47 | +const props = defineProps({ | ||
| 48 | + /** | ||
| 49 | + * 是否显示弹窗 | ||
| 50 | + * @type {boolean} | ||
| 51 | + */ | ||
| 52 | + visible: { | ||
| 53 | + type: Boolean, | ||
| 54 | + default: false | ||
| 55 | + }, | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 产品对象 | ||
| 59 | + * @type {Object} | ||
| 60 | + * @property {number} id - 产品 ID | ||
| 61 | + * @property {string} product_name - 产品名称 | ||
| 62 | + * @property {string} form_sn - 模版标识(必需) | ||
| 63 | + * @property {Object} plan_config - 模版配置(可选,后端返回) | ||
| 64 | + */ | ||
| 65 | + product: { | ||
| 66 | + type: Object, | ||
| 67 | + required: true | ||
| 68 | + } | ||
| 69 | +}) | ||
| 70 | + | ||
| 71 | +/** | ||
| 72 | + * 组件事件 | ||
| 73 | + */ | ||
| 74 | +const emit = defineEmits([ | ||
| 75 | + /** | ||
| 76 | + * 更新显示状态事件 | ||
| 77 | + * @event update:visible | ||
| 78 | + * @param {boolean} value - 显示状态 | ||
| 79 | + */ | ||
| 80 | + 'update:visible', | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 关闭事件 | ||
| 84 | + * @event close | ||
| 85 | + */ | ||
| 86 | + 'close', | ||
| 87 | + | ||
| 88 | + /** | ||
| 89 | + * 提交事件 | ||
| 90 | + * @event submit | ||
| 91 | + * @param {Object} formData - 表单数据 | ||
| 92 | + */ | ||
| 93 | + 'submit' | ||
| 94 | +]) | ||
| 95 | + | ||
| 96 | +/** | ||
| 97 | + * 当前模版配置 | ||
| 98 | + * @description 根据 form_sn 从配置文件中查找,并合并后端 plan_config | ||
| 99 | + * @type {ComputedRef<Object|null>} | ||
| 100 | + * | ||
| 101 | + * @example | ||
| 102 | + * // product.form_sn = 'life-insurance-wiop3e' | ||
| 103 | + * // templateConfig() 返回: { | ||
| 104 | + * // name: 'WIOP3E...', | ||
| 105 | + * // component: 'LifeInsuranceTemplate', | ||
| 106 | + * // config: { currency: 'USD', ... } | ||
| 107 | + * // } | ||
| 108 | + */ | ||
| 109 | +const templateConfig = computed(() => { | ||
| 110 | + if (!props.product?.form_sn) { | ||
| 111 | + console.warn('[PlanFormContainer] 产品缺少 form_sn 字段', props.product) | ||
| 112 | + return null | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + // 从配置文件中查找模版 | ||
| 116 | + const config = PLAN_TEMPLATES[props.product.form_sn] | ||
| 117 | + if (!config) { | ||
| 118 | + console.error(`[PlanFormContainer] 未找到模版配置: ${props.product.form_sn}`) | ||
| 119 | + return null | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + // 合并配置:优先使用后端返回的 plan_config,否则使用配置文件中的默认配置 | ||
| 123 | + return { | ||
| 124 | + ...config, | ||
| 125 | + config: { | ||
| 126 | + ...config.config, | ||
| 127 | + ...(props.product.plan_config || {}) | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | +}) | ||
| 131 | + | ||
| 132 | +/** | ||
| 133 | + * 当前模版组件 | ||
| 134 | + * @description 根据 component 名称动态加载对应的组件 | ||
| 135 | + * @type {ComputedRef<Component|null>} | ||
| 136 | + */ | ||
| 137 | +const currentTemplateComponent = computed(() => { | ||
| 138 | + if (!templateConfig.value) return null | ||
| 139 | + | ||
| 140 | + const componentMap = { | ||
| 141 | + 'LifeInsuranceTemplate': LifeInsuranceTemplate, | ||
| 142 | + 'CriticalIllnessTemplate': CriticalIllnessTemplate, | ||
| 143 | + 'SavingsTemplate': SavingsTemplate | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + const componentName = templateConfig.value.component | ||
| 147 | + return componentMap[componentName] || null | ||
| 148 | +}) | ||
| 149 | + | ||
| 150 | +/** | ||
| 151 | + * 表单数据 | ||
| 152 | + * @type {Ref<Object>} | ||
| 153 | + */ | ||
| 154 | +const formData = ref({}) | ||
| 155 | + | ||
| 156 | +/** | ||
| 157 | + * 监听产品变化,重置表单数据 | ||
| 158 | + */ | ||
| 159 | +watch( | ||
| 160 | + () => props.product, | ||
| 161 | + (newProduct) => { | ||
| 162 | + if (newProduct) { | ||
| 163 | + // 重置表单数据 | ||
| 164 | + formData.value = {} | ||
| 165 | + } | ||
| 166 | + }, | ||
| 167 | + { immediate: true } | ||
| 168 | +) | ||
| 169 | + | ||
| 170 | +/** | ||
| 171 | + * 监听显示状态变化 | ||
| 172 | + */ | ||
| 173 | +watch( | ||
| 174 | + () => props.visible, | ||
| 175 | + (newVal) => { | ||
| 176 | + emit('update:visible', newVal) | ||
| 177 | + } | ||
| 178 | +) | ||
| 179 | + | ||
| 180 | +/** | ||
| 181 | + * 关闭弹窗 | ||
| 182 | + */ | ||
| 183 | +const close = () => { | ||
| 184 | + emit('close') | ||
| 185 | +} | ||
| 186 | + | ||
| 187 | +/** | ||
| 188 | + * 提交表单 | ||
| 189 | + * @description 将表单数据和产品信息一起提交 | ||
| 190 | + */ | ||
| 191 | +const submit = () => { | ||
| 192 | + console.log('[PlanFormContainer] 提交计划书:', { | ||
| 193 | + product_id: props.product.id, | ||
| 194 | + product_name: props.product.product_name, | ||
| 195 | + form_sn: props.product.form_sn, | ||
| 196 | + form_data: formData.value | ||
| 197 | + }) | ||
| 198 | + | ||
| 199 | + emit('submit', { | ||
| 200 | + product_id: props.product.id, | ||
| 201 | + form_sn: props.product.form_sn, | ||
| 202 | + form_data: formData.value | ||
| 203 | + }) | ||
| 204 | +} | ||
| 205 | +</script> | ||
| 206 | + | ||
| 207 | +<style lang="less" scoped> | ||
| 208 | +/* 容器样式 */ | ||
| 209 | +</style> |
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 性别 --> | ||
| 4 | + <PlanFieldRadio | ||
| 5 | + v-model="form.gender" | ||
| 6 | + label="性别" | ||
| 7 | + :options="['男', '女']" | ||
| 8 | + /> | ||
| 9 | + | ||
| 10 | + <!-- 年龄(根据出生日期自动计算,可编辑) --> | ||
| 11 | + <PlanFieldAgePicker | ||
| 12 | + v-model="form.age" | ||
| 13 | + label="年龄" | ||
| 14 | + placeholder="请选择出生日期自动计算" | ||
| 15 | + /> | ||
| 16 | + | ||
| 17 | + <!-- 出生年月日 --> | ||
| 18 | + <PlanFieldDatePicker | ||
| 19 | + v-model="form.birthday" | ||
| 20 | + label="出生年月日" | ||
| 21 | + placeholder="请选择日期" | ||
| 22 | + @change="onBirthdayChange" | ||
| 23 | + /> | ||
| 24 | + | ||
| 25 | + <!-- 是否吸烟 --> | ||
| 26 | + <PlanFieldRadio | ||
| 27 | + v-model="form.smoker" | ||
| 28 | + label="是否吸烟" | ||
| 29 | + :options="['是', '否']" | ||
| 30 | + /> | ||
| 31 | + | ||
| 32 | + <!-- 保额 --> | ||
| 33 | + <PlanFieldAmount | ||
| 34 | + v-model="form.coverage" | ||
| 35 | + label="保额" | ||
| 36 | + placeholder="请输入保额" | ||
| 37 | + :currency="config.currency" | ||
| 38 | + /> | ||
| 39 | + | ||
| 40 | + <!-- 缴费年期 --> | ||
| 41 | + <PlanFieldSelect | ||
| 42 | + v-model="form.payment_period" | ||
| 43 | + label="缴费年期" | ||
| 44 | + placeholder="请选择缴费年期" | ||
| 45 | + :options="config.payment_periods" | ||
| 46 | + /> | ||
| 47 | + | ||
| 48 | + <!-- 保险期间 --> | ||
| 49 | + <div class="flex justify-between items-start mb-5"> | ||
| 50 | + <span class="text-sm text-gray-600 mt-1.5">保险期间</span> | ||
| 51 | + <div class="bg-blue-50 rounded-md px-3 py-1.5"> | ||
| 52 | + <span class="text-sm text-blue-600">{{ config.insurance_period }}</span> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + </div> | ||
| 56 | +</template> | ||
| 57 | + | ||
| 58 | +<script setup> | ||
| 59 | +/** | ||
| 60 | + * 重疾保险计划书模版 | ||
| 61 | + * | ||
| 62 | + * @description MPC/MBC PRO/MBC2 等重疾保险产品的计划书录入表单 | ||
| 63 | + * - 支持出生日期自动计算年龄 | ||
| 64 | + * - 表单字段:性别、年龄、出生年月日、是否吸烟、保额、缴费年期 | ||
| 65 | + * @author Claude Code | ||
| 66 | + * @example | ||
| 67 | + * <CriticalIllnessTemplate | ||
| 68 | + * v-model="formData" | ||
| 69 | + * :config="templateConfig" | ||
| 70 | + * /> | ||
| 71 | + */ | ||
| 72 | +import { reactive, watch } from 'vue' | ||
| 73 | +import PlanFieldAgePicker from '../PlanFields/AgePicker.vue' | ||
| 74 | +import PlanFieldAmount from '../PlanFields/AmountInput.vue' | ||
| 75 | +import PlanFieldDatePicker from '../PlanFields/DatePicker.vue' | ||
| 76 | +import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | ||
| 77 | +import PlanFieldSelect from '../PlanFields/SelectPicker.vue' | ||
| 78 | + | ||
| 79 | +/** | ||
| 80 | + * 组件属性 | ||
| 81 | + */ | ||
| 82 | +const props = defineProps({ | ||
| 83 | + /** | ||
| 84 | + * 表单数据对象 | ||
| 85 | + * @type {Object} | ||
| 86 | + */ | ||
| 87 | + modelValue: { | ||
| 88 | + type: Object, | ||
| 89 | + default: () => ({}) | ||
| 90 | + }, | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 模版配置 | ||
| 94 | + * @type {Object} | ||
| 95 | + * @property {string} currency - 币种代码 | ||
| 96 | + * @property {Array<string>} payment_periods - 缴费年期选项 | ||
| 97 | + * @property {Object} age_range - 年龄范围 { min, max } | ||
| 98 | + * @property {string} insurance_period - 保险期间 | ||
| 99 | + */ | ||
| 100 | + config: { | ||
| 101 | + type: Object, | ||
| 102 | + required: true | ||
| 103 | + } | ||
| 104 | +}) | ||
| 105 | + | ||
| 106 | +/** | ||
| 107 | + * 组件事件 | ||
| 108 | + */ | ||
| 109 | +const emit = defineEmits([ | ||
| 110 | + /** | ||
| 111 | + * 更新表单数据事件 | ||
| 112 | + * @event update:modelValue | ||
| 113 | + * @param {Object} value - 表单数据 | ||
| 114 | + */ | ||
| 115 | + 'update:modelValue' | ||
| 116 | +]) | ||
| 117 | + | ||
| 118 | +/** | ||
| 119 | + * 表单数据 | ||
| 120 | + * @type {Object} | ||
| 121 | + */ | ||
| 122 | +const form = reactive(props.modelValue || {}) | ||
| 123 | + | ||
| 124 | +/** | ||
| 125 | + * 监听表单数据变化,同步到父组件 | ||
| 126 | + */ | ||
| 127 | +watch( | ||
| 128 | + () => form, | ||
| 129 | + (newVal) => emit('update:modelValue', newVal), | ||
| 130 | + { deep: true } | ||
| 131 | +) | ||
| 132 | + | ||
| 133 | +/** | ||
| 134 | + * 出生日期变化时自动计算年龄 | ||
| 135 | + * @param {string} birthday - 出生日期(格式:YYYY-MM-DD) | ||
| 136 | + * | ||
| 137 | + * @description 用户选择出生日期后,自动计算并填充年龄字段 | ||
| 138 | + * 计算公式:当前年份 - 出生年份 | ||
| 139 | + */ | ||
| 140 | +const onBirthdayChange = (birthday) => { | ||
| 141 | + if (birthday) { | ||
| 142 | + const birthYear = new Date(birthday).getFullYear() | ||
| 143 | + const currentYear = new Date().getFullYear() | ||
| 144 | + const calculatedAge = currentYear - birthYear | ||
| 145 | + | ||
| 146 | + // 自动填充年龄字段 | ||
| 147 | + form.age = calculatedAge | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | +</script> | ||
| 151 | + | ||
| 152 | +<style lang="less" scoped> | ||
| 153 | +/* 模版样式 */ | ||
| 154 | +</style> |
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 性别 --> | ||
| 4 | + <PlanFieldRadio | ||
| 5 | + v-model="form.gender" | ||
| 6 | + label="性别" | ||
| 7 | + :options="['男', '女']" | ||
| 8 | + /> | ||
| 9 | + | ||
| 10 | + <!-- 年龄(根据出生日期自动计算,可编辑) --> | ||
| 11 | + <PlanFieldAgePicker | ||
| 12 | + v-model="form.age" | ||
| 13 | + label="年龄" | ||
| 14 | + placeholder="请选择出生日期自动计算" | ||
| 15 | + /> | ||
| 16 | + | ||
| 17 | + <!-- 出生年月日 --> | ||
| 18 | + <PlanFieldDatePicker | ||
| 19 | + v-model="form.birthday" | ||
| 20 | + label="出生年月日" | ||
| 21 | + placeholder="请选择日期" | ||
| 22 | + @change="onBirthdayChange" | ||
| 23 | + /> | ||
| 24 | + | ||
| 25 | + <!-- 是否吸烟 --> | ||
| 26 | + <PlanFieldRadio | ||
| 27 | + v-model="form.smoker" | ||
| 28 | + label="是否吸烟" | ||
| 29 | + :options="['是', '否']" | ||
| 30 | + /> | ||
| 31 | + | ||
| 32 | + <!-- 保额 --> | ||
| 33 | + <PlanFieldAmount | ||
| 34 | + v-model="form.coverage" | ||
| 35 | + label="保额" | ||
| 36 | + placeholder="请输入保额" | ||
| 37 | + :currency="config.currency" | ||
| 38 | + /> | ||
| 39 | + | ||
| 40 | + <!-- 缴费年期 --> | ||
| 41 | + <PlanFieldSelect | ||
| 42 | + v-model="form.payment_period" | ||
| 43 | + label="缴费年期" | ||
| 44 | + placeholder="请选择缴费年期" | ||
| 45 | + :options="config.payment_periods" | ||
| 46 | + /> | ||
| 47 | + | ||
| 48 | + <!-- 保险期间 --> | ||
| 49 | + <div class="flex justify-between items-start mb-5"> | ||
| 50 | + <span class="text-sm text-gray-600 mt-1.5">保险期间</span> | ||
| 51 | + <div class="bg-blue-50 rounded-md px-3 py-1.5"> | ||
| 52 | + <span class="text-sm text-blue-600">{{ config.insurance_period }}</span> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + </div> | ||
| 56 | +</template> | ||
| 57 | + | ||
| 58 | +<script setup> | ||
| 59 | +/** | ||
| 60 | + * 人寿保险计划书模版 | ||
| 61 | + * | ||
| 62 | + * @description WIOP3E/WIOP3 等人寿保险产品的计划书录入表单 | ||
| 63 | + * - 支持出生日期自动计算年龄 | ||
| 64 | + * - 表单字段:性别、年龄、出生年月日、是否吸烟、保额、缴费年期 | ||
| 65 | + * @author Claude Code | ||
| 66 | + * @example | ||
| 67 | + * <LifeInsuranceTemplate | ||
| 68 | + * v-model="formData" | ||
| 69 | + * :config="templateConfig" | ||
| 70 | + * /> | ||
| 71 | + */ | ||
| 72 | +import { reactive, watch, toRefs } from 'vue' | ||
| 73 | +import PlanFieldAgePicker from '../PlanFields/AgePicker.vue' | ||
| 74 | +import PlanFieldAmount from '../PlanFields/AmountInput.vue' | ||
| 75 | +import PlanFieldDatePicker from '../PlanFields/DatePicker.vue' | ||
| 76 | +import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | ||
| 77 | +import PlanFieldSelect from '../PlanFields/SelectPicker.vue' | ||
| 78 | + | ||
| 79 | +/** | ||
| 80 | + * 组件属性 | ||
| 81 | + */ | ||
| 82 | +const props = defineProps({ | ||
| 83 | + /** | ||
| 84 | + * 表单数据对象 | ||
| 85 | + * @type {Object} | ||
| 86 | + */ | ||
| 87 | + modelValue: { | ||
| 88 | + type: Object, | ||
| 89 | + default: () => ({}) | ||
| 90 | + }, | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 模版配置 | ||
| 94 | + * @type {Object} | ||
| 95 | + * @property {string} currency - 币种代码 | ||
| 96 | + * @property {Array<string>} payment_periods - 缴费年期选项 | ||
| 97 | + * @property {Object} age_range - 年龄范围 { min, max } | ||
| 98 | + * @property {string} insurance_period - 保险期间 | ||
| 99 | + */ | ||
| 100 | + config: { | ||
| 101 | + type: Object, | ||
| 102 | + required: true | ||
| 103 | + } | ||
| 104 | +}) | ||
| 105 | + | ||
| 106 | +/** | ||
| 107 | + * 组件事件 | ||
| 108 | + */ | ||
| 109 | +const emit = defineEmits([ | ||
| 110 | + /** | ||
| 111 | + * 更新表单数据事件 | ||
| 112 | + * @event update:modelValue | ||
| 113 | + * @param {Object} value - 表单数据 | ||
| 114 | + */ | ||
| 115 | + 'update:modelValue' | ||
| 116 | +]) | ||
| 117 | + | ||
| 118 | +/** | ||
| 119 | + * 表单数据 | ||
| 120 | + * @type {Object} | ||
| 121 | + */ | ||
| 122 | +const form = reactive(props.modelValue || {}) | ||
| 123 | + | ||
| 124 | +/** | ||
| 125 | + * 监听表单数据变化,同步到父组件 | ||
| 126 | + */ | ||
| 127 | +watch( | ||
| 128 | + () => form, | ||
| 129 | + (newVal) => emit('update:modelValue', newVal), | ||
| 130 | + { deep: true } | ||
| 131 | +) | ||
| 132 | + | ||
| 133 | +/** | ||
| 134 | + * 出生日期变化时自动计算年龄 | ||
| 135 | + * @param {string} birthday - 出生日期(格式:YYYY-MM-DD) | ||
| 136 | + * | ||
| 137 | + * @description 用户选择出生日期后,自动计算并填充年龄字段 | ||
| 138 | + * 计算公式:当前年份 - 出生年份 | ||
| 139 | + */ | ||
| 140 | +const onBirthdayChange = (birthday) => { | ||
| 141 | + if (birthday) { | ||
| 142 | + const birthYear = new Date(birthday).getFullYear() | ||
| 143 | + const currentYear = new Date().getFullYear() | ||
| 144 | + const calculatedAge = currentYear - birthYear | ||
| 145 | + | ||
| 146 | + // 自动填充年龄字段 | ||
| 147 | + form.age = calculatedAge | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | +</script> | ||
| 151 | + | ||
| 152 | +<style lang="less" scoped> | ||
| 153 | +/* 模版样式 */ | ||
| 154 | +</style> |
src/components/SavingsTemplate.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 性别 --> | ||
| 4 | + <PlanFieldRadio | ||
| 5 | + v-model="form.gender" | ||
| 6 | + label="性别" | ||
| 7 | + :options="['男', '女']" | ||
| 8 | + /> | ||
| 9 | + | ||
| 10 | + <!-- 年龄(根据出生日期自动计算,可编辑) --> | ||
| 11 | + <PlanFieldAgePicker | ||
| 12 | + v-model="form.age" | ||
| 13 | + label="年龄" | ||
| 14 | + placeholder="请选择出生日期自动计算" | ||
| 15 | + /> | ||
| 16 | + | ||
| 17 | + <!-- 出生年月日 --> | ||
| 18 | + <PlanFieldDatePicker | ||
| 19 | + v-model="form.birthday" | ||
| 20 | + label="出生年月日" | ||
| 21 | + placeholder="请选择日期" | ||
| 22 | + @change="onBirthdayChange" | ||
| 23 | + /> | ||
| 24 | + | ||
| 25 | + <!-- 是否吸烟 --> | ||
| 26 | + <PlanFieldRadio | ||
| 27 | + v-model="form.smoker" | ||
| 28 | + label="是否吸烟" | ||
| 29 | + :options="['是', '否']" | ||
| 30 | + /> | ||
| 31 | + | ||
| 32 | + <!-- 保额 --> | ||
| 33 | + <PlanFieldAmount | ||
| 34 | + v-model="form.coverage" | ||
| 35 | + label="保额" | ||
| 36 | + placeholder="请输入保额" | ||
| 37 | + :currency="config.currency" | ||
| 38 | + /> | ||
| 39 | + | ||
| 40 | + <!-- 缴费年期 --> | ||
| 41 | + <PlanFieldSelect | ||
| 42 | + v-model="form.payment_period" | ||
| 43 | + label="缴费年期" | ||
| 44 | + placeholder="请选择缴费年期" | ||
| 45 | + :options="config.payment_periods" | ||
| 46 | + /> | ||
| 47 | + | ||
| 48 | + <!-- 保险期间 --> | ||
| 49 | + <div class="flex justify-between items-start mb-5"> | ||
| 50 | + <span class="text-sm text-gray-600 mt-1.5">保险期间</span> | ||
| 51 | + <div class="bg-blue-50 rounded-md px-3 py-1.5"> | ||
| 52 | + <span class="text-sm text-blue-600">{{ config.insurance_period }}</span> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + | ||
| 56 | + <!-- ====== 提取计划功能(储蓄产品专用)====== --> | ||
| 57 | + <div v-if="config.withdrawal_plan?.enabled" class="mt-6 pt-6 border-t border-gray-200"> | ||
| 58 | + <div class="text-base font-medium text-gray-900 mb-4">提取计划</div> | ||
| 59 | + | ||
| 60 | + <!-- 提取方式选择 --> | ||
| 61 | + <PlanFieldRadio | ||
| 62 | + v-model="form.withdrawal_plan.mode" | ||
| 63 | + label="提取方式" | ||
| 64 | + :options="config.withdrawal_plan.withdrawal_modes" | ||
| 65 | + /> | ||
| 66 | + | ||
| 67 | + <!-- 开始年龄 --> | ||
| 68 | + <PlanFieldAgePicker | ||
| 69 | + v-model="form.withdrawal_plan.start_age" | ||
| 70 | + label="开始年龄" | ||
| 71 | + placeholder="请选择开始提取年龄" | ||
| 72 | + /> | ||
| 73 | + | ||
| 74 | + <!-- 提取年期 --> | ||
| 75 | + <PlanFieldSelect | ||
| 76 | + v-model="form.withdrawal_plan.withdrawal_period" | ||
| 77 | + label="提取年期" | ||
| 78 | + placeholder="请选择提取年期" | ||
| 79 | + :options="config.withdrawal_plan.withdrawal_periods" | ||
| 80 | + /> | ||
| 81 | + | ||
| 82 | + <!-- 方式1:年龄指定金额 - 额外字段 --> | ||
| 83 | + <template v-if="form.withdrawal_plan.mode === '年龄指定金额'"> | ||
| 84 | + <!-- 每年提取金额 --> | ||
| 85 | + <PlanFieldAmount | ||
| 86 | + v-model="form.withdrawal_plan.annual_amount" | ||
| 87 | + label="每年提取金额" | ||
| 88 | + placeholder="请输入金额" | ||
| 89 | + :currency="form.withdrawal_plan.currency || config.withdrawal_plan.default_currency" | ||
| 90 | + /> | ||
| 91 | + | ||
| 92 | + <!-- 币种 --> | ||
| 93 | + <div class="mb-5"> | ||
| 94 | + <div class="text-sm text-gray-600 mb-2">币种</div> | ||
| 95 | + <div class="flex gap-2"> | ||
| 96 | + <button | ||
| 97 | + v-for="curr in currencyOptions" | ||
| 98 | + :key="curr.value" | ||
| 99 | + :class="[ | ||
| 100 | + 'px-4 py-2 rounded-lg text-sm border transition-colors', | ||
| 101 | + (form.withdrawal_plan.currency || config.withdrawal_plan.default_currency) === curr.value | ||
| 102 | + ? 'bg-blue-600 text-white border-blue-600' | ||
| 103 | + : 'bg-white text-gray-600 border-gray-200' | ||
| 104 | + ]" | ||
| 105 | + @tap="selectCurrency(curr.value)" | ||
| 106 | + > | ||
| 107 | + {{ curr.label }} | ||
| 108 | + </button> | ||
| 109 | + </div> | ||
| 110 | + </div> | ||
| 111 | + | ||
| 112 | + <!-- 增加率 --> | ||
| 113 | + <div class="mb-5"> | ||
| 114 | + <div class="text-sm text-gray-600 mb-2">增加率(%)</div> | ||
| 115 | + <nut-input | ||
| 116 | + v-model="form.withdrawal_plan.increase_rate" | ||
| 117 | + type="digit" | ||
| 118 | + placeholder="请输入增加率" | ||
| 119 | + class="border border-gray-200 rounded-lg" | ||
| 120 | + /> | ||
| 121 | + </div> | ||
| 122 | + </template> | ||
| 123 | + </div> | ||
| 124 | + </div> | ||
| 125 | +</template> | ||
| 126 | + | ||
| 127 | +<script setup> | ||
| 128 | +/** | ||
| 129 | + * 储蓄型保险计划书模版 | ||
| 130 | + * | ||
| 131 | + * @description GS/GC/FA/LV2 等储蓄型保险产品的计划书录入表单 | ||
| 132 | + * - 支持出生日期自动计算年龄 | ||
| 133 | + * - 表单字段:性别、年龄、出生年月日、是否吸烟、保额、缴费年期 | ||
| 134 | + * - 提取计划功能:年龄指定金额、最高固定金额 | ||
| 135 | + * @author Claude Code | ||
| 136 | + * @example | ||
| 137 | + * <SavingsTemplate | ||
| 138 | + * v-model="formData" | ||
| 139 | + * :config="templateConfig" | ||
| 140 | + * /> | ||
| 141 | + */ | ||
| 142 | +import { reactive, watch, computed } from 'vue' | ||
| 143 | +import PlanFieldAgePicker from './PlanFields/AgePicker.vue' | ||
| 144 | +import PlanFieldAmount from './PlanFields/AmountInput.vue' | ||
| 145 | +import PlanFieldDatePicker from './PlanFields/DatePicker.vue' | ||
| 146 | +import PlanFieldRadio from './PlanFields/RadioGroup.vue' | ||
| 147 | +import PlanFieldSelect from './PlanFields/SelectPicker.vue' | ||
| 148 | + | ||
| 149 | +/** | ||
| 150 | + * 组件属性 | ||
| 151 | + */ | ||
| 152 | +const props = defineProps({ | ||
| 153 | + /** | ||
| 154 | + * 表单数据对象 | ||
| 155 | + * @type {Object} | ||
| 156 | + */ | ||
| 157 | + modelValue: { | ||
| 158 | + type: Object, | ||
| 159 | + default: () => ({}) | ||
| 160 | + }, | ||
| 161 | + | ||
| 162 | + /** | ||
| 163 | + * 模版配置 | ||
| 164 | + * @type {Object} | ||
| 165 | + * @property {string} currency - 币种代码 | ||
| 166 | + * @property {Array<string>} payment_periods - 缴费年期选项 | ||
| 167 | + * @property {Object} age_range - 年龄范围 { min, max } | ||
| 168 | + * @property {string} insurance_period - 保险期间 | ||
| 169 | + * @property {Object} withdrawal_plan - 提取计划配置 | ||
| 170 | + */ | ||
| 171 | + config: { | ||
| 172 | + type: Object, | ||
| 173 | + required: true | ||
| 174 | + } | ||
| 175 | +}) | ||
| 176 | + | ||
| 177 | +/** | ||
| 178 | + * 组件事件 | ||
| 179 | + */ | ||
| 180 | +const emit = defineEmits([ | ||
| 181 | + /** | ||
| 182 | + * 更新表单数据事件 | ||
| 183 | + * @event update:modelValue | ||
| 184 | + * @param {Object} value - 表单数据 | ||
| 185 | + */ | ||
| 186 | + 'update:modelValue' | ||
| 187 | +]) | ||
| 188 | + | ||
| 189 | +/** | ||
| 190 | + * 表单数据 | ||
| 191 | + * @type {Object} | ||
| 192 | + */ | ||
| 193 | +const form = reactive(props.modelValue || { | ||
| 194 | + // 初始化提取计划数据 | ||
| 195 | + withdrawal_plan: { | ||
| 196 | + mode: '年龄指定金额', | ||
| 197 | + start_age: null, | ||
| 198 | + withdrawal_period: null, | ||
| 199 | + annual_amount: null, | ||
| 200 | + currency: props.config?.withdrawal_plan?.default_currency || 'HKD', | ||
| 201 | + increase_rate: 0 | ||
| 202 | + } | ||
| 203 | +}) | ||
| 204 | + | ||
| 205 | +/** | ||
| 206 | + * 币种选项(用于提取计划) | ||
| 207 | + * @type {ComputedRef<Array>} | ||
| 208 | + */ | ||
| 209 | +const currencyOptions = computed(() => { | ||
| 210 | + const CURRENCY_MAP = { | ||
| 211 | + HKD: { label: '港币', value: 'HKD' }, | ||
| 212 | + USD: { label: '美元', value: 'USD' }, | ||
| 213 | + CNY: { label: '人民币', value: 'CNY' } | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + const supportedCurrencies = props.config?.withdrawal_plan?.currencies || ['HKD'] | ||
| 217 | + return supportedCurrencies | ||
| 218 | + .map(code => CURRENCY_MAP[code]) | ||
| 219 | + .filter(Boolean) | ||
| 220 | +}) | ||
| 221 | + | ||
| 222 | +/** | ||
| 223 | + * 监听表单数据变化,同步到父组件 | ||
| 224 | + */ | ||
| 225 | +watch( | ||
| 226 | + () => form, | ||
| 227 | + (newVal) => emit('update:modelValue', newVal), | ||
| 228 | + { deep: true } | ||
| 229 | +) | ||
| 230 | + | ||
| 231 | +/** | ||
| 232 | + * 出生日期变化时自动计算年龄 | ||
| 233 | + * @param {string} birthday - 出生日期(格式:YYYY-MM-DD) | ||
| 234 | + * | ||
| 235 | + * @description 用户选择出生日期后,自动计算并填充年龄字段 | ||
| 236 | + * 计算公式:当前年份 - 出生年份 | ||
| 237 | + */ | ||
| 238 | +const onBirthdayChange = (birthday) => { | ||
| 239 | + if (birthday) { | ||
| 240 | + const birthYear = new Date(birthday).getFullYear() | ||
| 241 | + const currentYear = new Date().getFullYear() | ||
| 242 | + const calculatedAge = currentYear - birthYear | ||
| 243 | + | ||
| 244 | + // 自动填充年龄字段 | ||
| 245 | + form.age = calculatedAge | ||
| 246 | + } | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +/** | ||
| 250 | + * 选择币种(用于提取计划) | ||
| 251 | + * @param {string} currencyCode - 币种代码 | ||
| 252 | + */ | ||
| 253 | +const selectCurrency = (currencyCode) => { | ||
| 254 | + if (form.withdrawal_plan) { | ||
| 255 | + form.withdrawal_plan.currency = currencyCode | ||
| 256 | + } | ||
| 257 | +} | ||
| 258 | +</script> | ||
| 259 | + | ||
| 260 | +<style lang="less" scoped> | ||
| 261 | +/* 模版样式 */ | ||
| 262 | +</style> |
src/config/plan-templates.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书模版配置 | ||
| 3 | + * | ||
| 4 | + * @description 定义产品 form_sn 到模版组件和配置的映射关系 | ||
| 5 | + * @module config/plan-templates | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-06 | ||
| 8 | + */ | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 计划书模版配置映射 | ||
| 12 | + * @description form_sn 为产品 API 返回的字段,用于标识该产品使用的计划书模版 | ||
| 13 | + * | ||
| 14 | + * @example | ||
| 15 | + * // 产品 API 返回 | ||
| 16 | + * { | ||
| 17 | + * id: 1, | ||
| 18 | + * product_name: "WIOP3E 盈传创富保障计划 3 - 优选版", | ||
| 19 | + * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key | ||
| 20 | + * } | ||
| 21 | + */ | ||
| 22 | +export const PLAN_TEMPLATES = { | ||
| 23 | + // 人寿保险产品 - WIOP3E | ||
| 24 | + 'life-insurance-wiop3e': { | ||
| 25 | + name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 26 | + component: 'LifeInsuranceTemplate', | ||
| 27 | + config: { | ||
| 28 | + currency: 'USD', // 币种:USD/CNY/HKD/EUR | ||
| 29 | + payment_periods: [ | ||
| 30 | + // 缴费年期选项 | ||
| 31 | + '整付(0-75 岁)', | ||
| 32 | + '5 年(0-70 岁)', | ||
| 33 | + '10 年(0-70 岁)' | ||
| 34 | + ], | ||
| 35 | + age_range: { min: 0, max: 75 }, // 年龄范围 | ||
| 36 | + insurance_period: '终身' // 保险期间 | ||
| 37 | + } | ||
| 38 | + }, | ||
| 39 | + | ||
| 40 | + // 人寿保险产品 - WIOP3 | ||
| 41 | + 'life-insurance-wiop3': { | ||
| 42 | + name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 43 | + component: 'LifeInsuranceTemplate', | ||
| 44 | + config: { | ||
| 45 | + currency: 'USD', | ||
| 46 | + payment_periods: [ | ||
| 47 | + '整付(0-75 岁)', | ||
| 48 | + '5 年(0-70 岁)', | ||
| 49 | + '10 年(0-70 岁)' | ||
| 50 | + ], | ||
| 51 | + age_range: { min: 0, max: 75 }, | ||
| 52 | + insurance_period: '终身' | ||
| 53 | + } | ||
| 54 | + }, | ||
| 55 | + | ||
| 56 | + // 重疾保险产品 - MPC | ||
| 57 | + 'critical-illness-mpc': { | ||
| 58 | + name: 'MPC 守护无间重疾', | ||
| 59 | + component: 'CriticalIllnessTemplate', | ||
| 60 | + config: { | ||
| 61 | + currency: 'CNY', | ||
| 62 | + payment_periods: [ | ||
| 63 | + '10 年(15 日 - 65 岁)', | ||
| 64 | + '20 年(15 日 - 65 岁)', | ||
| 65 | + '25 年(15 日 - 60 岁)' | ||
| 66 | + ], | ||
| 67 | + age_range: { min: 0, max: 65 }, | ||
| 68 | + insurance_period: '终身' | ||
| 69 | + } | ||
| 70 | + }, | ||
| 71 | + | ||
| 72 | + // 重疾保险产品 - MBC PRO | ||
| 73 | + 'critical-illness-mbc-pro': { | ||
| 74 | + name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 75 | + component: 'CriticalIllnessTemplate', | ||
| 76 | + config: { | ||
| 77 | + currency: 'CNY', | ||
| 78 | + payment_periods: [ | ||
| 79 | + '10 年(15 日 - 65 岁)', | ||
| 80 | + '20 年(15 日 - 65 岁)', | ||
| 81 | + '25 年(15 日 - 60 岁)' | ||
| 82 | + ], | ||
| 83 | + age_range: { min: 0, max: 65 }, | ||
| 84 | + insurance_period: '终身' | ||
| 85 | + } | ||
| 86 | + }, | ||
| 87 | + | ||
| 88 | + // 重疾保险产品 - MBC2 | ||
| 89 | + 'critical-illness-mbc2': { | ||
| 90 | + name: 'MBC2 活跃人生重疾保 2', | ||
| 91 | + component: 'CriticalIllnessTemplate', | ||
| 92 | + config: { | ||
| 93 | + currency: 'CNY', | ||
| 94 | + payment_periods: [ | ||
| 95 | + '10 年(15 日 - 65 岁)', | ||
| 96 | + '20 年(15 日 - 65 岁)', | ||
| 97 | + '25 年(15 日 - 60 岁)' | ||
| 98 | + ], | ||
| 99 | + age_range: { min: 0, max: 65 }, | ||
| 100 | + insurance_period: '终身' | ||
| 101 | + } | ||
| 102 | + }, | ||
| 103 | + | ||
| 104 | + // ====== 储蓄型产品(统一逻辑) ====== | ||
| 105 | + | ||
| 106 | + // GS - 宏摯傳承保障計劃 | ||
| 107 | + 'savings-gs': { | ||
| 108 | + name: '宏摯傳承保障計劃', | ||
| 109 | + component: 'SavingsTemplate', | ||
| 110 | + category: 'savings', // 储蓄型产品 | ||
| 111 | + config: { | ||
| 112 | + currency: 'USD', // 默认美元 | ||
| 113 | + payment_periods: [ | ||
| 114 | + '整付(0-100 岁)', | ||
| 115 | + '5 年(0-80 岁)', | ||
| 116 | + '10 年(0-75 岁)' | ||
| 117 | + ], | ||
| 118 | + age_range: { min: 0, max: 100 }, | ||
| 119 | + insurance_period: '终身', | ||
| 120 | + // 提取计划配置 | ||
| 121 | + withdrawal_plan: { | ||
| 122 | + enabled: true, | ||
| 123 | + currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 124 | + default_currency: 'HKD', | ||
| 125 | + withdrawal_modes: [ | ||
| 126 | + '年龄指定金额', // 方式1 | ||
| 127 | + '最高固定金额' // 方式2 | ||
| 128 | + ], | ||
| 129 | + withdrawal_periods: [ | ||
| 130 | + '1年', | ||
| 131 | + '2年', | ||
| 132 | + '3年', | ||
| 133 | + '5年', | ||
| 134 | + '10年', | ||
| 135 | + '15年', | ||
| 136 | + '20年', | ||
| 137 | + '终身' | ||
| 138 | + ] | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + }, | ||
| 142 | + | ||
| 143 | + // GC - 宏摯家傳承保險計劃 | ||
| 144 | + 'savings-gc': { | ||
| 145 | + name: '宏摯家傳承保險計劃', | ||
| 146 | + component: 'SavingsTemplate', | ||
| 147 | + category: 'savings', | ||
| 148 | + config: { | ||
| 149 | + currency: 'USD', | ||
| 150 | + payment_periods: [ | ||
| 151 | + '整付(0-100 岁)', | ||
| 152 | + '5 年(0-80 岁)', | ||
| 153 | + '10 年(0-75 岁)' | ||
| 154 | + ], | ||
| 155 | + age_range: { min: 0, max: 100 }, | ||
| 156 | + insurance_period: '终身', | ||
| 157 | + withdrawal_plan: { | ||
| 158 | + enabled: true, | ||
| 159 | + currencies: ['HKD', 'USD', 'CNY'], | ||
| 160 | + default_currency: 'HKD', | ||
| 161 | + withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 162 | + withdrawal_periods: [ | ||
| 163 | + '1年', | ||
| 164 | + '2年', | ||
| 165 | + '3年', | ||
| 166 | + '5年', | ||
| 167 | + '10年', | ||
| 168 | + '15年', | ||
| 169 | + '20年', | ||
| 170 | + '终身' | ||
| 171 | + ] | ||
| 172 | + } | ||
| 173 | + } | ||
| 174 | + }, | ||
| 175 | + | ||
| 176 | + // FA - 宏浚傳承保障計劃 | ||
| 177 | + 'savings-fa': { | ||
| 178 | + name: '宏浚傳承保障計劃', | ||
| 179 | + component: 'SavingsTemplate', | ||
| 180 | + category: 'savings', | ||
| 181 | + config: { | ||
| 182 | + currency: 'USD', | ||
| 183 | + payment_periods: [ | ||
| 184 | + '整付(0-100 岁)', | ||
| 185 | + '5 年(0-80 岁)', | ||
| 186 | + '10 年(0-75 岁)' | ||
| 187 | + ], | ||
| 188 | + age_range: { min: 0, max: 100 }, | ||
| 189 | + insurance_period: '终身', | ||
| 190 | + withdrawal_plan: { | ||
| 191 | + enabled: true, | ||
| 192 | + currencies: ['HKD', 'USD', 'CNY'], | ||
| 193 | + default_currency: 'HKD', | ||
| 194 | + withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 195 | + withdrawal_periods: [ | ||
| 196 | + '1年', | ||
| 197 | + '2年', | ||
| 198 | + '3年', | ||
| 199 | + '5年', | ||
| 200 | + '10年', | ||
| 201 | + '15年', | ||
| 202 | + '20年', | ||
| 203 | + '终身' | ||
| 204 | + ] | ||
| 205 | + } | ||
| 206 | + } | ||
| 207 | + }, | ||
| 208 | + | ||
| 209 | + // LV2 - 赤霞珠終身壽險計劃2(储蓄型终身寿险) | ||
| 210 | + 'savings-lv2': { | ||
| 211 | + name: '赤霞珠終身壽險計劃2', | ||
| 212 | + component: 'SavingsTemplate', | ||
| 213 | + category: 'savings', | ||
| 214 | + config: { | ||
| 215 | + currency: 'USD', | ||
| 216 | + payment_periods: [ | ||
| 217 | + '整付(0-100 岁)', | ||
| 218 | + '5 年(0-80 岁)', | ||
| 219 | + '10 年(0-75 岁)' | ||
| 220 | + ], | ||
| 221 | + age_range: { min: 0, max: 100 }, | ||
| 222 | + insurance_period: '终身', | ||
| 223 | + withdrawal_plan: { | ||
| 224 | + enabled: true, | ||
| 225 | + currencies: ['HKD', 'USD', 'CNY'], | ||
| 226 | + default_currency: 'HKD', | ||
| 227 | + withdrawal_modes: ['年龄指定金额', '最高固定金额'], | ||
| 228 | + withdrawal_periods: [ | ||
| 229 | + '1年', | ||
| 230 | + '2年', | ||
| 231 | + '3年', | ||
| 232 | + '5年', | ||
| 233 | + '10年', | ||
| 234 | + '15年', | ||
| 235 | + '20年', | ||
| 236 | + '终身' | ||
| 237 | + ] | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | + } | ||
| 241 | +} | ||
| 242 | + | ||
| 243 | +/** | ||
| 244 | + * 全局功能开关 | ||
| 245 | + * @description 用于控制实验性功能或未来扩展功能的开关 | ||
| 246 | + * | ||
| 247 | + * @example 开启多币种功能:设置 MULTI_CURRENCY_ENABLED = true | ||
| 248 | + */ | ||
| 249 | +export const FEATURE_FLAGS = { | ||
| 250 | + /** | ||
| 251 | + * 多币种切换功能 | ||
| 252 | + * @description false: 方案 1 - 固定币种(当前实现) | ||
| 253 | + * true: 方案 2 - 支持多币种切换(未来扩展) | ||
| 254 | + * @type {boolean} | ||
| 255 | + */ | ||
| 256 | + MULTI_CURRENCY_ENABLED: false | ||
| 257 | +} | ||
| 258 | + | ||
| 259 | +/** | ||
| 260 | + * 币种符号映射 | ||
| 261 | + * @description 币种代码到符号的映射关系 | ||
| 262 | + */ | ||
| 263 | +export const CURRENCY_SYMBOLS = { | ||
| 264 | + CNY: '¥', // 人民币 | ||
| 265 | + USD: '$', // 美元 | ||
| 266 | + HKD: 'HK$', // 港币 | ||
| 267 | + EUR: '€' // 欧元 | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +/** | ||
| 271 | + * 币种完整信息映射 | ||
| 272 | + * @description 币种代码到完整信息的映射(用于多币种模式) | ||
| 273 | + */ | ||
| 274 | +export const CURRENCY_MAP = { | ||
| 275 | + CNY: { label: '人民币', symbol: '¥', value: 'CNY' }, | ||
| 276 | + USD: { label: '美元', symbol: '$', value: 'USD' }, | ||
| 277 | + HKD: { label: '港币', symbol: 'HK$', value: 'HKD' }, | ||
| 278 | + EUR: { label: '欧元', symbol: '€', value: 'EUR' } | ||
| 279 | +} | ||
| 280 | + | ||
| 281 | +/** | ||
| 282 | + * 根据 form_sn 获取模版配置 | ||
| 283 | + * @param {string} formSn - 产品 API 返回的 form_sn 字段 | ||
| 284 | + * @returns {Object|null} 模版配置对象,未找到返回 null | ||
| 285 | + * | ||
| 286 | + * @example | ||
| 287 | + * const config = getTemplateConfig('life-insurance-wiop3e') | ||
| 288 | + * // 返回: { name: 'WIOP3E...', component: 'LifeInsuranceTemplate', config: {...} } | ||
| 289 | + */ | ||
| 290 | +export function getTemplateConfig(formSn) { | ||
| 291 | + if (!formSn) { | ||
| 292 | + console.warn('[plan-templates] form_sn 为空') | ||
| 293 | + return null | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + const config = PLAN_TEMPLATES[formSn] | ||
| 297 | + if (!config) { | ||
| 298 | + console.error(`[plan-templates] 未找到模版配置: ${formSn}`) | ||
| 299 | + return null | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + return config | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +/** | ||
| 306 | + * 获取币种符号 | ||
| 307 | + * @param {string} currencyCode - 币种代码(CNY/USD/HKD/EUR) | ||
| 308 | + * @returns {string} 币种符号 | ||
| 309 | + * | ||
| 310 | + * @example | ||
| 311 | + * const symbol = getCurrencySymbol('USD') // 返回: '$' | ||
| 312 | + */ | ||
| 313 | +export function getCurrencySymbol(currencyCode) { | ||
| 314 | + return CURRENCY_SYMBOLS[currencyCode] || '¥' | ||
| 315 | +} |
| ... | @@ -42,7 +42,7 @@ | ... | @@ -42,7 +42,7 @@ |
| 42 | <view class="bg-white rounded-[32rpx] shadow-sm p-[32rpx] mb-[24rpx]"> | 42 | <view class="bg-white rounded-[32rpx] shadow-sm p-[32rpx] mb-[24rpx]"> |
| 43 | <view class="flex justify-between items-center mb-[24rpx]"> | 43 | <view class="flex justify-between items-center mb-[24rpx]"> |
| 44 | <text class="text-gray-900 text-[32rpx] font-bold">热卖产品</text> | 44 | <text class="text-gray-900 text-[32rpx] font-bold">热卖产品</text> |
| 45 | - <view class="flex items-center text-blue-600" @tap="go('/pages/knowledge-base/index')"> | 45 | + <view class="flex items-center text-blue-600" @tap="go('/pages/product-center/index')"> |
| 46 | <text class="text-[26rpx] mr-[4rpx]">查看更多</text> | 46 | <text class="text-[26rpx] mr-[4rpx]">查看更多</text> |
| 47 | <IconFont name="rectRight" size="12" /> | 47 | <IconFont name="rectRight" size="12" /> |
| 48 | </view> | 48 | </view> |
| ... | @@ -159,19 +159,14 @@ | ... | @@ -159,19 +159,14 @@ |
| 159 | <!-- Bottom Tab Bar --> | 159 | <!-- Bottom Tab Bar --> |
| 160 | <TabBar current="home" /> | 160 | <TabBar current="home" /> |
| 161 | 161 | ||
| 162 | - <!-- Plan Popup --> | 162 | + <!-- Plan Form Container --> |
| 163 | - <PlanPopup v-model:visible="showPlanPopup"> | 163 | + <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> |
| 164 | - <SchemeA | 164 | + <PlanFormContainer |
| 165 | - v-if="currentScheme === 'A'" | 165 | + v-model:visible="showPlanPopup" |
| 166 | - @close="showPlanPopup = false" | 166 | + :product="selectedProduct" |
| 167 | - @submit="handlePlanSubmit" | 167 | + @close="showPlanPopup = false" |
| 168 | - /> | 168 | + @submit="handlePlanSubmit" |
| 169 | - <SchemeB | 169 | + /> |
| 170 | - v-if="currentScheme === 'B'" | ||
| 171 | - @close="showPlanPopup = false" | ||
| 172 | - @submit="handlePlanSubmit" | ||
| 173 | - /> | ||
| 174 | - </PlanPopup> | ||
| 175 | </view> | 170 | </view> |
| 176 | </template> | 171 | </template> |
| 177 | 172 | ||
| ... | @@ -184,9 +179,7 @@ import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'; | ... | @@ -184,9 +179,7 @@ import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'; |
| 184 | import { useUserStore } from '@/stores/user'; | 179 | import { useUserStore } from '@/stores/user'; |
| 185 | import TabBar from '@/components/TabBar.vue'; | 180 | import TabBar from '@/components/TabBar.vue'; |
| 186 | import IconFont from '@/components/IconFont.vue'; | 181 | import IconFont from '@/components/IconFont.vue'; |
| 187 | -import PlanPopup from '@/components/PlanPopup/index.vue'; | 182 | +import PlanFormContainer from '@/components/PlanFormContainer.vue'; |
| 188 | -import SchemeA from '@/components/PlanSchemes/SchemeA.vue'; | ||
| 189 | -import SchemeB from '@/components/PlanSchemes/SchemeB.vue'; | ||
| 190 | import ListItemActions from '@/components/ListItemActions/index.vue'; | 183 | import ListItemActions from '@/components/ListItemActions/index.vue'; |
| 191 | import { listAPI } from '@/api/get_product'; | 184 | import { listAPI } from '@/api/get_product'; |
| 192 | import { weekHotAPI } from '@/api/file'; | 185 | import { weekHotAPI } from '@/api/file'; |
| ... | @@ -198,26 +191,57 @@ const userStore = useUserStore(); | ... | @@ -198,26 +191,57 @@ const userStore = useUserStore(); |
| 198 | 191 | ||
| 199 | // Plan Popup State | 192 | // Plan Popup State |
| 200 | const showPlanPopup = ref(false); | 193 | const showPlanPopup = ref(false); |
| 201 | -const currentScheme = ref('A'); | 194 | +const selectedProduct = ref(null); |
| 202 | 195 | ||
| 203 | -const openPlanPopup = (scheme) => { | 196 | +/** |
| 204 | - currentScheme.value = scheme; | 197 | + * 打开计划书弹窗 |
| 198 | + * @description 根据产品ID找到对应的产品对象,并打开计划书表单 | ||
| 199 | + * @param {number} productId - 产品ID | ||
| 200 | + */ | ||
| 201 | +const openPlanPopup = (productId) => { | ||
| 202 | + // 从热卖产品列表中找到对应的产品 | ||
| 203 | + const product = hotProducts.value.find(p => p.id === productId); | ||
| 204 | + | ||
| 205 | + if (!product) { | ||
| 206 | + Taro.showToast({ | ||
| 207 | + title: '产品不存在', | ||
| 208 | + icon: 'none', | ||
| 209 | + duration: 2000 | ||
| 210 | + }); | ||
| 211 | + return; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + // 设置选中的产品 | ||
| 215 | + selectedProduct.value = product; | ||
| 205 | showPlanPopup.value = true; | 216 | showPlanPopup.value = true; |
| 206 | }; | 217 | }; |
| 207 | 218 | ||
| 208 | /** | 219 | /** |
| 209 | * 处理计划书提交 | 220 | * 处理计划书提交 |
| 210 | - * @description 模拟提交计划书,跳转到结果页面 | 221 | + * @description 测试环境:前端不调用后端API,直接跳转到结果页 |
| 222 | + * 生产环境:需要调用 submitPlanAPI 提交表单数据 | ||
| 211 | * @param {Object} formData - 表单数据 | 223 | * @param {Object} formData - 表单数据 |
| 212 | */ | 224 | */ |
| 213 | const handlePlanSubmit = (formData) => { | 225 | const handlePlanSubmit = (formData) => { |
| 214 | - console.log(`方案${currentScheme.value}提交:`, formData); | 226 | + console.log('计划书提交:', { |
| 227 | + product_id: selectedProduct.value.id, | ||
| 228 | + product_name: selectedProduct.value.product_name, | ||
| 229 | + form_sn: selectedProduct.value.form_sn, | ||
| 230 | + form_data: formData | ||
| 231 | + }); | ||
| 215 | 232 | ||
| 216 | // 关闭弹窗 | 233 | // 关闭弹窗 |
| 217 | showPlanPopup.value = false; | 234 | showPlanPopup.value = false; |
| 218 | 235 | ||
| 236 | + // TODO: 后端接口还没有准备好,暂时不调用API | ||
| 237 | + // 测试完成后需要对接 submitPlanAPI | ||
| 238 | + // const res = await submitPlanAPI({ | ||
| 239 | + // product_id: selectedProduct.value.id, | ||
| 240 | + // template: selectedProduct.value.form_sn, | ||
| 241 | + // form_data: formData | ||
| 242 | + // }); | ||
| 243 | + | ||
| 219 | // 模拟提交成功,跳转到结果页面 | 244 | // 模拟提交成功,跳转到结果页面 |
| 220 | - // TODO: 后续接入真实API | ||
| 221 | go('/pages/plan-submit-result/index', { | 245 | go('/pages/plan-submit-result/index', { |
| 222 | success: 'true' | 246 | success: 'true' |
| 223 | }); | 247 | }); |
| ... | @@ -269,7 +293,7 @@ const fetchHomeIcons = async () => { | ... | @@ -269,7 +293,7 @@ const fetchHomeIcons = async () => { |
| 269 | // 如果 API 调用失败,使用默认配置 | 293 | // 如果 API 调用失败,使用默认配置 |
| 270 | loopNav.value = [ | 294 | loopNav.value = [ |
| 271 | { id: 'plan', icon: 'order', name: '计划书', route: '/pages/plan/index' }, | 295 | { id: 'plan', icon: 'order', name: '计划书', route: '/pages/plan/index' }, |
| 272 | - { id: 'knowledge-base', icon: 'category', name: '产品知识库', route: '/pages/knowledge-base/index' } | 296 | + { id: 'product-center', icon: 'category', name: '产品中心', route: '/pages/product-center/index' } |
| 273 | ]; | 297 | ]; |
| 274 | } | 298 | } |
| 275 | }; | 299 | }; |
| ... | @@ -284,17 +308,119 @@ const hotProducts = ref([]); | ... | @@ -284,17 +308,119 @@ const hotProducts = ref([]); |
| 284 | /** | 308 | /** |
| 285 | * 获取热卖产品列表 | 309 | * 获取热卖产品列表 |
| 286 | * | 310 | * |
| 287 | - * @description 调用产品列表API,recommend参数为hot | 311 | + * @description ⚠️ 测试数据:后端接口和字段还没有准备好,暂时使用模拟数据进行测试 |
| 312 | + * 测试完成后需要移除,恢复使用真实的API调用 | ||
| 313 | + * Mock数据包含全部7种产品类型(2种人寿、3种重疾、4种储蓄) | ||
| 288 | */ | 314 | */ |
| 289 | const fetchHotProducts = async () => { | 315 | const fetchHotProducts = async () => { |
| 290 | try { | 316 | try { |
| 291 | - const res = await listAPI({ | 317 | + // TODO: 测试完成后,移除下面的 mock 数据,恢复使用真实 API |
| 292 | - recommend: 'hot' | 318 | + // const res = await listAPI({ |
| 293 | - }); | 319 | + // recommend: 'hot' |
| 320 | + // }); | ||
| 321 | + // if (res.code === 1 && res.data && res.data.list) { | ||
| 322 | + // hotProducts.value = res.data.list; | ||
| 323 | + // } | ||
| 324 | + | ||
| 325 | + // ⚠️ 测试数据开始 - 测试完成后需要移除 ⚠️ | ||
| 326 | + hotProducts.value = [ | ||
| 327 | + // 人寿保险产品(2种) | ||
| 328 | + { | ||
| 329 | + id: 1, | ||
| 330 | + product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 331 | + form_sn: 'life-insurance-wiop3e', | ||
| 332 | + recommend: 'hot', | ||
| 333 | + tags: [ | ||
| 334 | + { id: 1, name: '终身寿险', bg_color: '#DBEAFE', text_color: '#1E40AF' }, | ||
| 335 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 336 | + ] | ||
| 337 | + }, | ||
| 338 | + { | ||
| 339 | + id: 2, | ||
| 340 | + product_name: 'WIOP3 - 盈传创富保障计划 3', | ||
| 341 | + form_sn: 'life-insurance-wiop3', | ||
| 342 | + recommend: 'hot', | ||
| 343 | + tags: [ | ||
| 344 | + { id: 1, name: '终身寿险', bg_color: '#DBEAFE', text_color: '#1E40AF' }, | ||
| 345 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 346 | + ] | ||
| 347 | + }, | ||
| 348 | + // 重疾保险产品(3种) | ||
| 349 | + { | ||
| 350 | + id: 3, | ||
| 351 | + product_name: 'MPC 守护无间重疾', | ||
| 352 | + form_sn: 'critical-illness-mpc', | ||
| 353 | + recommend: 'hot', | ||
| 354 | + tags: [ | ||
| 355 | + { id: 1, name: '重疾保障', bg_color: '#FCE7F3', text_color: '#9F1239' }, | ||
| 356 | + { id: 2, name: '人民币', bg_color: '#D1FAE5', text_color: '#065F46' } | ||
| 357 | + ] | ||
| 358 | + }, | ||
| 359 | + { | ||
| 360 | + id: 4, | ||
| 361 | + product_name: 'MBC PRO 活跃人生重疾保 PRO', | ||
| 362 | + form_sn: 'critical-illness-mbc-pro', | ||
| 363 | + recommend: 'hot', | ||
| 364 | + tags: [ | ||
| 365 | + { id: 1, name: '重疾保障', bg_color: '#FCE7F3', text_color: '#9F1239' }, | ||
| 366 | + { id: 2, name: '人民币', bg_color: '#D1FAE5', text_color: '#065F46' } | ||
| 367 | + ] | ||
| 368 | + }, | ||
| 369 | + { | ||
| 370 | + id: 5, | ||
| 371 | + product_name: 'MBC2 活跃人生重疾保 2', | ||
| 372 | + form_sn: 'critical-illness-mbc2', | ||
| 373 | + recommend: 'hot', | ||
| 374 | + tags: [ | ||
| 375 | + { id: 1, name: '重疾保障', bg_color: '#FCE7F3', text_color: '#9F1239' }, | ||
| 376 | + { id: 2, name: '人民币', bg_color: '#D1FAE5', text_color: '#065F46' } | ||
| 377 | + ] | ||
| 378 | + }, | ||
| 379 | + // 储蓄型产品(4种)- GS, GC, FA, LV2 | ||
| 380 | + { | ||
| 381 | + id: 6, | ||
| 382 | + product_name: 'GS - 宏摯傳承保障計劃', | ||
| 383 | + form_sn: 'savings-gs', | ||
| 384 | + recommend: 'hot', | ||
| 385 | + tags: [ | ||
| 386 | + { id: 1, name: '储蓄分红', bg_color: '#E0E7FF', text_color: '#3730A3' }, | ||
| 387 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 388 | + ] | ||
| 389 | + }, | ||
| 390 | + { | ||
| 391 | + id: 7, | ||
| 392 | + product_name: 'GC - 宏摯家傳承保險計劃', | ||
| 393 | + form_sn: 'savings-gc', | ||
| 394 | + recommend: 'hot', | ||
| 395 | + tags: [ | ||
| 396 | + { id: 1, name: '储蓄分红', bg_color: '#E0E7FF', text_color: '#3730A3' }, | ||
| 397 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 398 | + ] | ||
| 399 | + }, | ||
| 400 | + { | ||
| 401 | + id: 8, | ||
| 402 | + product_name: 'FA - 宏浚傳承保障計劃', | ||
| 403 | + form_sn: 'savings-fa', | ||
| 404 | + recommend: 'hot', | ||
| 405 | + tags: [ | ||
| 406 | + { id: 1, name: '储蓄分红', bg_color: '#E0E7FF', text_color: '#3730A3' }, | ||
| 407 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 408 | + ] | ||
| 409 | + }, | ||
| 410 | + { | ||
| 411 | + id: 9, | ||
| 412 | + product_name: 'LV2 - 赤霞珠終身壽險計劃2', | ||
| 413 | + form_sn: 'savings-lv2', | ||
| 414 | + recommend: 'hot', | ||
| 415 | + tags: [ | ||
| 416 | + { id: 1, name: '储蓄型终身寿险', bg_color: '#E0E7FF', text_color: '#3730A3' }, | ||
| 417 | + { id: 2, name: '美元', bg_color: '#FEF3C7', text_color: '#92400E' } | ||
| 418 | + ] | ||
| 419 | + } | ||
| 420 | + ]; | ||
| 421 | + // ⚠️ 测试数据结束 - 测试完成后需要移除 ⚠️ | ||
| 294 | 422 | ||
| 295 | - if (res.code === 1 && res.data && res.data.list) { | 423 | + console.log('⚠️ 使用测试数据:热卖产品列表已 mock,包含全部7种产品类型'); |
| 296 | - hotProducts.value = res.data.list; | ||
| 297 | - } | ||
| 298 | } catch (err) { | 424 | } catch (err) { |
| 299 | console.error('获取热卖产品失败:', err); | 425 | console.error('获取热卖产品失败:', err); |
| 300 | } | 426 | } | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2026-01-29 21:53:42 | 2 | * @Date: 2026-01-29 21:53:42 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-31 10:53:44 | 4 | + * @LastEditTime: 2026-02-06 11:55:01 |
| 5 | * @FilePath: /manulife-weapp/src/pages/knowledge-base/index.config.js | 5 | * @FilePath: /manulife-weapp/src/pages/knowledge-base/index.config.js |
| 6 | - * @Description: 产品知识库页面配置文件 | 6 | + * @Description: 产品中心页面配置文件 |
| 7 | */ | 7 | */ |
| 8 | export default definePageConfig({ | 8 | export default definePageConfig({ |
| 9 | - navigationBarTitleText: '产品知识库', | 9 | + navigationBarTitleText: '产品中心', |
| 10 | navigationStyle: 'custom' | 10 | navigationStyle: 'custom' |
| 11 | }) | 11 | }) | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2026-01-31 | 2 | * @Date: 2026-01-31 |
| 3 | - * @Description: 产品知识库 - API 接口集成版本(含搜索功能) | 3 | + * @Description: 产品中心 - API 接口集成版本(含搜索功能) |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | <view class="h-screen bg-[#F9FAFB] flex flex-col"> | 6 | <view class="h-screen bg-[#F9FAFB] flex flex-col"> |
| 7 | <view class="bg-[#F9FAFB] z-10"> | 7 | <view class="bg-[#F9FAFB] z-10"> |
| 8 | - <NavHeader title="产品知识库" /> | 8 | + <NavHeader title="产品中心" /> |
| 9 | 9 | ||
| 10 | <!-- Search Bar --> | 10 | <!-- Search Bar --> |
| 11 | <view class="px-[24rpx] py-[16rpx] bg-white"> | 11 | <view class="px-[24rpx] py-[16rpx] bg-white"> |
| ... | @@ -79,7 +79,7 @@ | ... | @@ -79,7 +79,7 @@ |
| 79 | </view> | 79 | </view> |
| 80 | 80 | ||
| 81 | <!-- 动态标签 --> | 81 | <!-- 动态标签 --> |
| 82 | - <view v-if="item.tags && item.tags.length > 0" class="flex flex-wrap gap-[8rpx] mt-auto"> | 82 | + <view v-if="item.tags && item.tags.length > 0" class="flex flex-wrap gap-[8rpx] mb-[16rpx]"> |
| 83 | <view | 83 | <view |
| 84 | v-for="tag in item.tags.slice(0, 2)" | 84 | v-for="tag in item.tags.slice(0, 2)" |
| 85 | :key="tag.id" | 85 | :key="tag.id" |
| ... | @@ -92,6 +92,27 @@ | ... | @@ -92,6 +92,27 @@ |
| 92 | {{ tag.name }} | 92 | {{ tag.name }} |
| 93 | </view> | 93 | </view> |
| 94 | </view> | 94 | </view> |
| 95 | + | ||
| 96 | + <!-- 按钮组 --> | ||
| 97 | + <view class="flex gap-[12rpx] mt-auto"> | ||
| 98 | + <nut-button | ||
| 99 | + plain | ||
| 100 | + color="#2563EB" | ||
| 101 | + size="small" | ||
| 102 | + class="flex-1 !h-[56rpx] !rounded-[12rpx] !text-[22rpx] !m-0 !border-blue-600" | ||
| 103 | + @tap.stop="handleProductClick(item)" | ||
| 104 | + > | ||
| 105 | + 详情 | ||
| 106 | + </nut-button> | ||
| 107 | + <nut-button | ||
| 108 | + color="#2563EB" | ||
| 109 | + size="small" | ||
| 110 | + class="flex-1 !h-[56rpx] !rounded-[12rpx] !text-[22rpx] !m-0" | ||
| 111 | + @tap.stop="openPlanPopup(item)" | ||
| 112 | + > | ||
| 113 | + 计划书 | ||
| 114 | + </nut-button> | ||
| 115 | + </view> | ||
| 95 | </view> | 116 | </view> |
| 96 | </view> | 117 | </view> |
| 97 | </view> | 118 | </view> |
| ... | @@ -111,6 +132,15 @@ | ... | @@ -111,6 +132,15 @@ |
| 111 | <nut-empty description="暂无相关产品" image="empty" /> | 132 | <nut-empty description="暂无相关产品" image="empty" /> |
| 112 | </view> | 133 | </view> |
| 113 | </scroll-view> | 134 | </scroll-view> |
| 135 | + | ||
| 136 | + <!-- 计划书表单容器 --> | ||
| 137 | + <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> | ||
| 138 | + <PlanFormContainer | ||
| 139 | + v-model:visible="showPlanPopup" | ||
| 140 | + :product="selectedProduct" | ||
| 141 | + @close="showPlanPopup = false" | ||
| 142 | + @submit="handlePlanSubmit" | ||
| 143 | + /> | ||
| 114 | </view> | 144 | </view> |
| 115 | </template> | 145 | </template> |
| 116 | 146 | ||
| ... | @@ -119,6 +149,7 @@ import { ref, computed } from 'vue' | ... | @@ -119,6 +149,7 @@ import { ref, computed } from 'vue' |
| 119 | import Taro, { useLoad, useReachBottom } from '@tarojs/taro' | 149 | import Taro, { useLoad, useReachBottom } from '@tarojs/taro' |
| 120 | import NavHeader from '@/components/NavHeader.vue' | 150 | import NavHeader from '@/components/NavHeader.vue' |
| 121 | import SearchBar from '@/components/SearchBar.vue' | 151 | import SearchBar from '@/components/SearchBar.vue' |
| 152 | +import PlanFormContainer from '@/components/PlanFormContainer.vue' | ||
| 122 | import { useListItemClick, ListType } from '@/composables/useListItemClick' | 153 | import { useListItemClick, ListType } from '@/composables/useListItemClick' |
| 123 | import { listAPI } from '@/api/get_product' | 154 | import { listAPI } from '@/api/get_product' |
| 124 | 155 | ||
| ... | @@ -140,6 +171,10 @@ const categories = ref([]) // 从接口获取的分类列表 | ... | @@ -140,6 +171,10 @@ const categories = ref([]) // 从接口获取的分类列表 |
| 140 | const products = ref([]) // 当前产品列表 | 171 | const products = ref([]) // 当前产品列表 |
| 141 | const total = ref(0) // 产品总数 | 172 | const total = ref(0) // 产品总数 |
| 142 | 173 | ||
| 174 | +// 计划书弹窗状态 | ||
| 175 | +const showPlanPopup = ref(false) | ||
| 176 | +const selectedProduct = ref(null) | ||
| 177 | + | ||
| 143 | /** | 178 | /** |
| 144 | * 标签栏数据(根据接口返回的 categories 生成) | 179 | * 标签栏数据(根据接口返回的 categories 生成) |
| 145 | * @description 包含"全部"选项和接口返回的分类 | 180 | * @description 包含"全部"选项和接口返回的分类 |
| ... | @@ -326,6 +361,47 @@ const { handleClick: handleProductClick } = useListItemClick({ | ... | @@ -326,6 +361,47 @@ const { handleClick: handleProductClick } = useListItemClick({ |
| 326 | }) | 361 | }) |
| 327 | 362 | ||
| 328 | /** | 363 | /** |
| 364 | + * 打开计划书弹窗 | ||
| 365 | + * @description 根据产品对象打开计划书表单 | ||
| 366 | + * @param {Object} product - 产品对象 | ||
| 367 | + */ | ||
| 368 | +const openPlanPopup = (product) => { | ||
| 369 | + selectedProduct.value = product | ||
| 370 | + showPlanPopup.value = true | ||
| 371 | +} | ||
| 372 | + | ||
| 373 | +/** | ||
| 374 | + * 处理计划书提交 | ||
| 375 | + * @description 测试环境:前端不调用后端API,直接跳转到结果页 | ||
| 376 | + * 生产环境:需要调用 submitPlanAPI 提交表单数据 | ||
| 377 | + * @param {Object} formData - 表单数据 | ||
| 378 | + */ | ||
| 379 | +const handlePlanSubmit = (formData) => { | ||
| 380 | + console.log('计划书提交:', { | ||
| 381 | + product_id: selectedProduct.value.id, | ||
| 382 | + product_name: selectedProduct.value.product_name, | ||
| 383 | + form_sn: selectedProduct.value.form_sn, | ||
| 384 | + form_data: formData | ||
| 385 | + }) | ||
| 386 | + | ||
| 387 | + // 关闭弹窗 | ||
| 388 | + showPlanPopup.value = false | ||
| 389 | + | ||
| 390 | + // TODO: 后端接口还没有准备好,暂时不调用API | ||
| 391 | + // 测试完成后需要对接 submitPlanAPI | ||
| 392 | + // const res = await submitPlanAPI({ | ||
| 393 | + // product_id: selectedProduct.value.id, | ||
| 394 | + // template: selectedProduct.value.form_sn, | ||
| 395 | + // form_data: formData | ||
| 396 | + // }) | ||
| 397 | + | ||
| 398 | + // 模拟提交成功,跳转到结果页面 | ||
| 399 | + Taro.navigateTo({ | ||
| 400 | + url: '/pages/plan-submit-result/index?success=true' | ||
| 401 | + }) | ||
| 402 | +} | ||
| 403 | + | ||
| 404 | +/** | ||
| 329 | * 页面加载时获取数据 | 405 | * 页面加载时获取数据 |
| 330 | */ | 406 | */ |
| 331 | useLoad(() => { | 407 | useLoad(() => { | ... | ... |
| ... | @@ -98,6 +98,26 @@ | ... | @@ -98,6 +98,26 @@ |
| 98 | 98 | ||
| 99 | <!-- TabBar --> | 99 | <!-- TabBar --> |
| 100 | <!-- <TabBar /> --> | 100 | <!-- <TabBar /> --> |
| 101 | + | ||
| 102 | + <!-- 计划书按钮 --> | ||
| 103 | + <div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-[32rpx] py-[24rpx] flex items-center justify-center"> | ||
| 104 | + <nut-button | ||
| 105 | + color="#2563EB" | ||
| 106 | + class="!w-full !h-[88rpx] !rounded-[16rpx] !text-[28rpx] !font-bold" | ||
| 107 | + @tap="openPlanPopup" | ||
| 108 | + > | ||
| 109 | + 制作计划书 | ||
| 110 | + </nut-button> | ||
| 111 | + </div> | ||
| 112 | + | ||
| 113 | + <!-- 计划书表单容器 --> | ||
| 114 | + <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> | ||
| 115 | + <PlanFormContainer | ||
| 116 | + v-model:visible="showPlanPopup" | ||
| 117 | + :product="productDetail" | ||
| 118 | + @close="showPlanPopup = false" | ||
| 119 | + @submit="handlePlanSubmit" | ||
| 120 | + /> | ||
| 101 | </div> | 121 | </div> |
| 102 | </template> | 122 | </template> |
| 103 | 123 | ||
| ... | @@ -106,6 +126,7 @@ import { ref } from 'vue' | ... | @@ -106,6 +126,7 @@ import { ref } from 'vue' |
| 106 | import NavHeader from '@/components/NavHeader.vue' | 126 | import NavHeader from '@/components/NavHeader.vue' |
| 107 | import TabBar from '@/components/TabBar.vue' | 127 | import TabBar from '@/components/TabBar.vue' |
| 108 | import IconFont from '@/components/IconFont.vue' | 128 | import IconFont from '@/components/IconFont.vue' |
| 129 | +import PlanFormContainer from '@/components/PlanFormContainer.vue' | ||
| 109 | import { useFileOperation } from '@/composables/useFileOperation' | 130 | import { useFileOperation } from '@/composables/useFileOperation' |
| 110 | import Taro, { useLoad } from '@tarojs/taro' | 131 | import Taro, { useLoad } from '@tarojs/taro' |
| 111 | import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' | 132 | import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' |
| ... | @@ -113,6 +134,9 @@ import { detailAPI } from '@/api/get_product' | ... | @@ -113,6 +134,9 @@ import { detailAPI } from '@/api/get_product' |
| 113 | 134 | ||
| 114 | const { viewFile } = useFileOperation() | 135 | const { viewFile } = useFileOperation() |
| 115 | 136 | ||
| 137 | +// 计划书弹窗状态 | ||
| 138 | +const showPlanPopup = ref(false) | ||
| 139 | + | ||
| 116 | // 接收页面参数 | 140 | // 接收页面参数 |
| 117 | const productId = ref(null) | 141 | const productId = ref(null) |
| 118 | 142 | ||
| ... | @@ -181,6 +205,47 @@ const viewDocument = (doc) => { | ... | @@ -181,6 +205,47 @@ const viewDocument = (doc) => { |
| 181 | }) | 205 | }) |
| 182 | } | 206 | } |
| 183 | 207 | ||
| 208 | +/** | ||
| 209 | + * 打开计划书弹窗 | ||
| 210 | + * | ||
| 211 | + * @description 打开当前产品的计划书表单 | ||
| 212 | + */ | ||
| 213 | +const openPlanPopup = () => { | ||
| 214 | + showPlanPopup.value = true | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +/** | ||
| 218 | + * 处理计划书提交 | ||
| 219 | + * | ||
| 220 | + * @description 测试环境:前端不调用后端API,直接跳转到结果页 | ||
| 221 | + * 生产环境:需要调用 submitPlanAPI 提交表单数据 | ||
| 222 | + * @param {Object} formData - 表单数据 | ||
| 223 | + */ | ||
| 224 | +const handlePlanSubmit = (formData) => { | ||
| 225 | + console.log('计划书提交:', { | ||
| 226 | + product_id: productDetail.value.id, | ||
| 227 | + product_name: productDetail.value.product_name, | ||
| 228 | + form_sn: productDetail.value.form_sn, | ||
| 229 | + form_data: formData | ||
| 230 | + }) | ||
| 231 | + | ||
| 232 | + // 关闭弹窗 | ||
| 233 | + showPlanPopup.value = false | ||
| 234 | + | ||
| 235 | + // TODO: 后端接口还没有准备好,暂时不调用API | ||
| 236 | + // 测试完成后需要对接 submitPlanAPI | ||
| 237 | + // const res = await submitPlanAPI({ | ||
| 238 | + // product_id: productDetail.value.id, | ||
| 239 | + // template: productDetail.value.form_sn, | ||
| 240 | + // form_data: formData | ||
| 241 | + // }) | ||
| 242 | + | ||
| 243 | + // 模拟提交成功,跳转到结果页面 | ||
| 244 | + Taro.navigateTo({ | ||
| 245 | + url: '/pages/plan-submit-result/index?success=true' | ||
| 246 | + }) | ||
| 247 | +} | ||
| 248 | + | ||
| 184 | useLoad((options) => { | 249 | useLoad((options) => { |
| 185 | console.log('产品详情页参数:', options) | 250 | console.log('产品详情页参数:', options) |
| 186 | 251 | ... | ... |
| ... | @@ -382,7 +382,7 @@ const clearSearch = () => { | ... | @@ -382,7 +382,7 @@ const clearSearch = () => { |
| 382 | // Go to detail | 382 | // Go to detail |
| 383 | const goToDetail = (item) => { | 383 | const goToDetail = (item) => { |
| 384 | if (item.category === 'product') { | 384 | if (item.category === 'product') { |
| 385 | - go('/pages/knowledge-base/index') | 385 | + go('/pages/product-center/index') |
| 386 | } else { | 386 | } else { |
| 387 | go('/pages/material-list/index', { title: '搜索结果' }) | 387 | go('/pages/material-list/index', { title: '搜索结果' }) |
| 388 | } | 388 | } | ... | ... |
-
Please register or login to post a comment