hookehuyr

feat(plan): 同步 develop 分支最新代码 - 计划书模块完整更新

- 添加文档解析工具 (scripts/parse-docs.js)
  - 优化计划书配置定位与 Schema 文档
  - 完整计划书字段验证、分组、转换系统
  - 添加计划书表单重构 (PlanFormContainer)
  - 补充计划书状态管理与颜色标识
  - 优化计划书模板 (储蓄/人寿/重疾)
  - 统一权限检查与文件操作反馈
  - 同步文档更新 (README/PLAN/CHANGELOG)

详见 docs/CHANGELOG.md

Co-Authored-By: Claude Code
Showing 57 changed files with 5048 additions and 1277 deletions
1 +{
2 + "types": ["feat", "fix", "perf"],
3 + "skip": {
4 + "changelog": true
5 + }
6 +}
...@@ -19,6 +19,60 @@ pnpm build:weapp # 构建生产版本(微信小程序) ...@@ -19,6 +19,60 @@ pnpm build:weapp # 构建生产版本(微信小程序)
19 pnpm lint # 运行 ESLint 19 pnpm lint # 运行 ESLint
20 ``` 20 ```
21 21
22 +### Git 工作流
23 +
24 +#### 从 develop 创建功能分支
25 +
26 +```bash
27 +# 1. 切换到 develop(确保最新)
28 +git checkout develop
29 +git pull
30 +
31 +# 2. 创建功能分支
32 +git checkout -b feature/功能名称
33 +
34 +# 3. 开发完成后,合并回 develop
35 +git checkout develop
36 +git merge feature/功能名称
37 +
38 +# 4. 删除功能分支(可选)
39 +git branch -d feature/功能名称
40 +```
41 +
42 +**分支命名规范**
43 +- `feature/xxx` - 新功能
44 +- `fix/xxx` - Bug 修复
45 +- `refactor/xxx` - 重构
46 +
47 +#### 版本自动更新(已实现)
48 +
49 +**规则**:遵循 Semantic Versioning
50 +- `feat` - MINOR 版本更新(1.0.0 → 1.1.0)
51 +- `fix` - PATCH 版本更新(1.0.0 → 1.0.1)
52 +- `perf` - MINOR 版本更新
53 +- `docs/style/refactor/test/chore` - 不更新
54 +
55 +**实现方式**
56 +-`commit-msg` hook �用 `scripts/update-version.sh` 自动更新
57 +- ✅ 更新后的 `package.json` 自动加入暂存区
58 +- ✅ 支持 `feat(version):` 格式跳过版本更新
59 +
60 +**使用示例**
61 +```bash
62 +# 在当前功能分支开发
63 +git checkout -b feature/new-page
64 +# ... 开发代码 ...
65 +git add .
66 +git commit -m "feat(page): 添加新页面"
67 +
68 +# 合并回 develop
69 +git checkout develop
70 +git merge feature/new-page
71 +
72 +# 删除分支(可选)
73 +git branch -d feature/new-page
74 +```
75 +
22 ### 其他平台构建 76 ### 其他平台构建
23 ```bash 77 ```bash
24 pnpm dev:alipay # 支付宝小程序开发 78 pnpm dev:alipay # 支付宝小程序开发
......
...@@ -76,7 +76,7 @@ export const submitFormAPI = (params) => { ...@@ -76,7 +76,7 @@ export const submitFormAPI = (params) => {
76 export default { 76 export default {
77 pages: [ 77 pages: [
78 'pages/index/index', // 首页 78 'pages/index/index', // 首页
79 - 'pages/auth/index', // 认证页(必须保留) 79 + 'pages/login/index', // 登录页(必须保留)
80 'pages/your-page/index', // 🔧 添加您的页面 80 'pages/your-page/index', // 🔧 添加您的页面
81 ], 81 ],
82 tabBar: { 82 tabBar: {
...@@ -120,9 +120,9 @@ pnpm dev:h5 ...@@ -120,9 +120,9 @@ pnpm dev:h5
120 120
121 - **静默认证**:应用启动时自动执行 121 - **静默认证**:应用启动时自动执行
122 - **401 自动刷新**:接口返回 401 时自动刷新会话 122 - **401 自动刷新**:接口返回 401 时自动刷新会话
123 -- **授权页回跳**:认证完成后自动返回原页面 123 +- **登录页回跳**:登录完成后自动返回原页面
124 124
125 -**重要**:后端需提供 `/srv/?a=openid_wxapp` 接口 125 +**重要**:后端需提供 `/srv/?a=openid` 接口
126 126
127 ### 🌐 网络请求 127 ### 🌐 网络请求
128 128
...@@ -203,7 +203,7 @@ export const useUserStore = defineStore('user', { ...@@ -203,7 +203,7 @@ export const useUserStore = defineStore('user', {
203 203
204 ### Q: 认证流程不工作? 204 ### Q: 认证流程不工作?
205 205
206 -1. 检查后端 `/srv/?a=openid_wxapp` 接口是否正常 206 +1. 检查后端 `/srv/?a=openid` 接口是否正常
207 2. 检查 `src/utils/config.js` 中的 `BASE_URL` 是否正确 207 2. 检查 `src/utils/config.js` 中的 `BASE_URL` 是否正确
208 3. 查看微信开发者工具控制台错误信息 208 3. 查看微信开发者工具控制台错误信息
209 209
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
6 6
7 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 7 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
8 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) 8 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
9 +- **[文档导航](docs/README.md)** - 项目文档索引与使用建议
10 +- **[计划书表单 Schema 使用文档](docs/plan/plan-form-schema-usage.md)** - 计划书表单配置与扩展指南
9 11
10 ## 🚀 快速开始 12 ## 🚀 快速开始
11 13
...@@ -44,6 +46,44 @@ pnpm lint ...@@ -44,6 +46,44 @@ pnpm lint
44 -**样式方案** - TailwindCSS(80%) + Less(20%) 混合使用 46 -**样式方案** - TailwindCSS(80%) + Less(20%) 混合使用
45 -**组件复用** - "第 3 次出现原则"抽取 Composables 47 -**组件复用** - "第 3 次出现原则"抽取 Composables
46 -**可复用组件** - TabBar、NavHeader、IconFont 48 -**可复用组件** - TabBar、NavHeader、IconFont
49 +-**文档预览能力** - DocumentPreview 支持多格式文件预览
50 +-**统一列表交互** - 搜索、收藏、资料列表统一点击与操作逻辑
51 +
52 +## 🆕 最新更新(2026-02-14)
53 +
54 +### 构建告警修复
55 +-**usePlanView 导出补齐** - 补充 usePlanView 导出并绑定 viewFile,修复构建告警
56 +
57 +### 计划书表单演进
58 +-**Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验
59 +-**提交映射下沉** - 提交字段映射从容器迁移到模板配置
60 +-**人寿/重疾同步** - 人寿与重疾模板改为 Schema 驱动
61 +-**校验提示优化** - 必填提示与百分比校验统一更准确
62 +
63 +### 字段命名优化
64 +-**提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method
65 +-**文档同步** - 更新提取计划相关文档字段示例
66 +-**优化建议** - 提取计划相关字段命名保持“功能优先”的语义一致性
67 +
68 +### 文档对齐
69 +-**业务模块更新** - README 页面清单与业务模块对齐现有路由
70 +-**新人指南更新** - 入口文档从工具生成器调整为业务上手流程
71 +-**文档导航同步** - docs/README 快速导航修正与补充
72 +
73 +### 计划书模块定位
74 +-**配置与入口整理** - 补充计划书模块入口、配置与 API 位置说明
75 +-**优化建议** - 新增产品时优先补齐 form_sn 与 plan_config,避免模板缺失
76 +
77 +### 计划书模块优化补齐
78 +-**字段分组补齐** - 补齐基本信息/保障/提取字段分组
79 +-**错误回调兼容** - 支持 onError 回调并保持 onViewError 兼容
80 +-**转换逻辑修正** - 分元双向转换统一使用转换器
81 +-**依赖检测测试** - 补充循环依赖检测单测与分组工具测试
82 +
83 +### 计划书配置核查
84 +-**配置应用核查** - 确认 plan-templates 已驱动表单渲染与提交映射,plan-fields 与字段关联/转换 composable 尚未接入生成链路
85 +-**依赖与转换接入** - 表单可见性接入 useFieldDependencies,提交金额转换接入 useFieldValueTransform
86 +-**提取字段拆分** - 指定提取金额与最高固定提取金额字段独立显示与提交映射
47 87
48 ## 🆕 最新更新(2026-02-13) 88 ## 🆕 最新更新(2026-02-13)
49 89
...@@ -126,46 +166,43 @@ pnpm lint ...@@ -126,46 +166,43 @@ pnpm lint
126 ``` 166 ```
127 src/ 167 src/
128 ├── api/ # API 接口层 168 ├── api/ # API 接口层
129 -│ ├── index.js # 业务接口定义
130 -│ └── fn.js # HTTP 请求封装
131 ├── assets/ # 静态资源 169 ├── assets/ # 静态资源
132 -│ ├── images/ # 图片资源
133 -│ └── styles/ # 全局样式
134 ├── components/ # 通用组件 170 ├── components/ # 通用组件
135 -│ ├── IconFont.vue # 图标字体组件 171 +│ ├── cards/ # 卡片组件
136 -│ ├── NavHeader.vue # 自定义导航头 172 +│ ├── documents/ # 文档预览组件
137 -│ ├── TabBar.vue # 底部导航栏 173 +│ ├── forms/ # 表单组件
138 -│ ├── SectionCard.vue # 分组卡片组件 174 +│ ├── icons/ # 图标组件
139 -│ ├── FilterTabs.vue # 筛选标签组件 175 +│ ├── list/ # 列表组件
140 -│ ├── PosterBuilder/ # 海报生成器(可选) 176 +│ ├── navigation/ # 导航组件
141 -│ └── PlanSchemes/ # 计划书方案组件 177 +│ └── plan/ # 计划书相关组件
142 -├── composables/ # Composition API hooks 178 +├── composables/ # 组合式函数
143 -│ ├── useSectionList.js # 分组列表管理 179 +├── config/ # 功能与权限配置
144 -│ ├── useFileOperation.js # 文件操作(下载、预览) 180 +├── hooks/ # hooks
145 -│ └── useListItemClick.js # 列表点击处理
146 -├── hooks/ # 自定义 hooks
147 -│ └── useGo.js # 增强导航 hook
148 ├── pages/ # 页面组件 181 ├── pages/ # 页面组件
149 │ ├── index/ # 首页 182 │ ├── index/ # 首页
150 -│ ├── auth/ # 认证页(必须保留) 183 +│ ├── product-center/ # 产品中心
151 -│ ├── login/ # 登录页
152 │ ├── product-detail/ # 产品详情 184 │ ├── product-detail/ # 产品详情
185 +│ ├── category-list/ # 分类列表
153 │ ├── material-list/ # 资料列表 186 │ ├── material-list/ # 资料列表
154 -│ ├── knowledge-base/ # 知识库 187 +│ ├── week-hot-material/ # 周热门资料
155 -│ ├── plan/ # 计划书 188 +│ ├── signing/ # 签单相关
189 +│ ├── family-office/ # 家办业务
190 +│ ├── plan/ # 计划书列表
191 +│ ├── plan-submit-result/ # 计划书提交结果
192 +│ ├── search/ # 搜索
193 +│ ├── document-preview/ # 文档预览
194 +│ ├── message/ # 消息列表
195 +│ ├── message-detail/ # 消息详情
196 +│ ├── feedback/ # 意见反馈
197 +│ ├── feedback-list/ # 反馈列表
156 │ ├── favorites/ # 我的收藏 198 │ ├── favorites/ # 我的收藏
157 │ ├── mine/ # 我的 199 │ ├── mine/ # 我的
158 -│ └── ... 200 +│ ├── avatar/ # 头像编辑
159 -├── stores/ # Pinia 状态管理 201 +│ ├── help-center/ # 帮助中心
160 -│ ├── router.js # 路由状态(认证回跳) 202 +│ ├── login/ # 登录
161 -│ ├── main.js # 主 store 203 +│ ├── onboarding/ # 引导页
162 -│ └── host.js # 配置 store 204 +│ ├── video-player/ # 视频播放
163 -├── utils/ # 工具函数 205 +│ └── webview/ # WebView 承载页
164 -│ ├── authRedirect.js # 认证流程核心(必须)
165 -│ ├── request.js # HTTP 客户端核心(必须)
166 -│ ├── config.js # 环境配置(⚠️ 需修改)
167 -│ ├── tools.js # 通用工具
168 -│ └── documentIcons.js # 文档图标工具
169 ├── app.js # 应用入口 206 ├── app.js # 应用入口
170 ├── app.config.js # 页面路由配置 207 ├── app.config.js # 页面路由配置
171 └── app.less # 全局样式 208 └── app.less # 全局样式
...@@ -173,17 +210,28 @@ src/ ...@@ -173,17 +210,28 @@ src/
173 210
174 ## 📄 页面说明 211 ## 📄 页面说明
175 212
176 -### 主要页面 213 +### 主要业务页面
177 -- **首页** - 产品展示、热门资料、导航入口 214 +- **首页** - 导航入口、产品与资料推荐
178 -- **产品详情** - 完整产品信息展示 215 +- **产品中心** - 产品聚合展示与筛选
179 -- **资料列表** - 培训材料和文档管理 216 +- **产品详情** - 产品信息与附件预览入口
180 -- **知识库** - 培训材料和案例知识库 217 +- **分类列表/资料列表** - 分类浏览与资料列表
181 -- **计划书** - 计划书列表和管理 218 +- **周热门资料** - 热门资料聚合列表
182 -- **我的收藏** - 收藏内容管理 219 +- **签单相关** - 业务签单资料入口
183 -- **我的** - 个人资料、功能菜单 220 +- **家办业务** - 家办相关资料入口
221 +- **计划书** - 计划书列表、状态展示
222 +- **计划书提交结果** - 提交完成提示与下一步引导
223 +- **搜索** - 全局搜索与结果分类
224 +
225 +### 辅助与管理页面
226 +- **消息列表/消息详情** - 消息通知与计划书状态查看
227 +- **文档预览/视频播放** - 文件预览与播放
228 +- **意见反馈/反馈列表** - 反馈提交与历史查看
229 +- **我的/头像/帮助中心** - 个人信息与常用入口
230 +- **登录/引导页** - 认证与首次引导
231 +- **WebView** - 承载外部 H5 内容
184 232
185 ### 页面特性 233 ### 页面特性
186 -- ✅ 顶部筛选固定,列表独立滚动 234 +-列表页顶部筛选固定,列表独立滚动
187 - ✅ 统一的导航头(NavHeader)和底部导航(TabBar) 235 - ✅ 统一的导航头(NavHeader)和底部导航(TabBar)
188 - ✅ 统一的文件操作逻辑(下载、预览) 236 - ✅ 统一的文件操作逻辑(下载、预览)
189 - ✅ 统一的列表点击处理 237 - ✅ 统一的列表点击处理
...@@ -223,7 +271,7 @@ export const yourAPI = (params) => { ...@@ -223,7 +271,7 @@ export const yourAPI = (params) => {
223 export default { 271 export default {
224 pages: [ 272 pages: [
225 'pages/index/index', 273 'pages/index/index',
226 - 'pages/auth/index', 274 + 'pages/login/index',
227 'pages/your-page/index', // 添加您的页面 275 'pages/your-page/index', // 添加您的页面
228 ], 276 ],
229 } 277 }
...@@ -242,13 +290,13 @@ export default { ...@@ -242,13 +290,13 @@ export default {
242 290
243 1. **静默认证**:应用启动时自动执行 291 1. **静默认证**:应用启动时自动执行
244 2. **401 自动刷新**:接口返回 401 时自动刷新会话 292 2. **401 自动刷新**:接口返回 401 时自动刷新会话
245 -3. **授权页回跳**:认证完成后自动跳转回原页面 293 +3. **登录页回跳**:登录完成后自动跳转回原页面
246 294
247 **核心文件** 295 **核心文件**
248 - `src/utils/authRedirect.js` - 认证流程管理 296 - `src/utils/authRedirect.js` - 认证流程管理
249 - `src/utils/request.js` - HTTP 请求拦截器 297 - `src/utils/request.js` - HTTP 请求拦截器
250 298
251 -**重要**:后端需提供 `/srv/?a=openid_wxapp` 接口用于微信登录。 299 +**重要**:后端需提供 `/srv/?a=openid` 接口用于微信登录。
252 300
253 ## 📦 技术栈 301 ## 📦 技术栈
254 302
...@@ -328,8 +376,15 @@ export default { ...@@ -328,8 +376,15 @@ export default {
328 2. 请求超时默认 5 秒,可在 `src/utils/request.js` 中修改 376 2. 请求超时默认 5 秒,可在 `src/utils/request.js` 中修改
329 3. NutUI 组件已配置自动导入,无需手动引入 377 3. NutUI 组件已配置自动导入,无需手动引入
330 4. TailwindCSS 已禁用 preflight,避免与小程序样式冲突 378 4. TailwindCSS 已禁用 preflight,避免与小程序样式冲突
331 -5. 认证失败会自动跳转到 `/pages/auth/index` 379 +5. 认证失败会自动跳转到 `/pages/login/index`
332 6. **所有函数必须有 JSDoc 注释** - 详见 `~/.claude/rules/code-commenting.md` 380 6. **所有函数必须有 JSDoc 注释** - 详见 `~/.claude/rules/code-commenting.md`
381 +7. 业务路由以 `src/app.config.js` 为准,计划类文档仅保留历史记录
382 +
383 +## ✅ 优化建议
384 +
385 +1. 持续维护 API 集成日志与页面模块对应关系
386 +2. 文档预览与视频播放页面补充更多异常场景说明
387 +3. 页面入口与权限策略保持同步,避免入口显示但权限不一致
333 388
334 ## 📄 License 389 ## 📄 License
335 390
......
This diff could not be displayed because it is too large.
...@@ -102,31 +102,27 @@ ...@@ -102,31 +102,27 @@
102 - 缴费年期:各产品不同(详见配置文件) 102 - 缴费年期:各产品不同(详见配置文件)
103 - **提取计划功能**(所有储蓄产品通用): 103 - **提取计划功能**(所有储蓄产品通用):
104 104
105 - **三层结构** 105 + **字段结构说明**
106 106
107 - **第一层**:是否希望生成一份容许减少名义金额的提取说明?(是/否) 107 + **字段1**:是否希望生成一份容许减少名义金额的提取说明?(是/否)
108 + - 独立字段,不影响下面的提取方案配置
109 + - 仅用于标识是否需要生成说明文档
108 110
109 - **第二层**(选择"是"时显示): 111 + **字段2**:提取选项(二选一):
110 - - 提取选项(二选一): 112 + - 指定提取金额
111 - 1. 指定提取金额 113 + - 最高固定提取金额
112 - 2. 最高固定提取金额
113 114
114 - **第三层**(根据第二层选择显示不同字段) 115 + **字段3-N**:根据字段2的选择显示不同字段
115 116
116 **A. 指定提取金额模式** 117 **A. 指定提取金额模式**
117 - - 提取方式(二选一) 118 + - 提取方式:
118 1. 按年岁 119 1. 按年岁
119 - 2. 按保单年度
120 120
121 - **按年岁**字段(3个): 121 - **按年岁**字段(3个):
122 - 由几岁开始(withdrawal_start_age) 122 - 由几岁开始(withdrawal_start_age)
123 - 提取期(年)(withdrawal_period) 123 - 提取期(年)(withdrawal_period)
124 - 每年递增提取之百分比(%)(increase_rate) 124 - 每年递增提取之百分比(%)(increase_rate)
125 125
126 - - **按保单年度**字段(2个):
127 - - 由几岁开始(withdrawal_start_age)
128 - - 提取期(年)(withdrawal_period)
129 -
130 **B. 最高固定提取金额模式**(2个字段): 126 **B. 最高固定提取金额模式**(2个字段):
131 - 按年岁:由几岁开始(withdrawal_start_age) 127 - 按年岁:由几岁开始(withdrawal_start_age)
132 - 提取期(年)(withdrawal_period) 128 - 提取期(年)(withdrawal_period)
...@@ -138,8 +134,7 @@ ...@@ -138,8 +134,7 @@
138 134
139 **字段清理逻辑** 135 **字段清理逻辑**
140 - 切换提取方式时,自动清除不相关字段 136 - 切换提取方式时,自动清除不相关字段
141 - - 切换"按年岁"和"按保单年度"时,清除 annual_amount 和 increase_rate 137 + - "是否希望生成说明"字段不影响任何其他字段
142 - - 选择"否"(不启用提取计划)时,清除所有提取计划相关字段
143 138
144 --- 139 ---
145 140
...@@ -387,15 +382,17 @@ src/ ...@@ -387,15 +382,17 @@ src/
387 382
388 **业务场景**:储蓄型产品(GS/GC/FA/LV2)支持提取计划功能 383 **业务场景**:储蓄型产品(GS/GC/FA/LV2)支持提取计划功能
389 384
390 -**三层结构** 385 +**字段结构**
391 386
392 -#### 第一层:启用确认 387 +#### 字段1:是否生成说明(独立字段)
393 388
394 **问题**:是否希望生成一份容许减少名义金额的提取说明? 389 **问题**:是否希望生成一份容许减少名义金额的提取说明?
395 390
396 **选项**:是 / 否(默认:否) 391 **选项**:是 / 否(默认:否)
397 392
398 -#### 第二层:提取选项(第一层选择"是"时显示) 393 +**说明**:此字段为独立配置,不影响下面的提取方案
394 +
395 +#### 字段2:提取选项
399 396
400 **问题**:提取选项 397 **问题**:提取选项
401 398
...@@ -403,7 +400,7 @@ src/ ...@@ -403,7 +400,7 @@ src/
403 1. 指定提取金额 400 1. 指定提取金额
404 2. 最高固定提取金额 401 2. 最高固定提取金额
405 402
406 -#### 第三层:具体字段(根据第二层选择显示不同字段) 403 +#### 字段3-N:具体配置字段(根据字段2选择显示不同字段)
407 404
408 ##### A. 指定提取金额模式 405 ##### A. 指定提取金额模式
409 406
...@@ -411,31 +408,19 @@ src/ ...@@ -411,31 +408,19 @@ src/
411 408
412 **选项** 409 **选项**
413 1. 按年岁 410 1. 按年岁
414 -2. 按保单年度
415 411
416 **按年岁字段**(3个): 412 **按年岁字段**(3个):
417 ```javascript 413 ```javascript
418 { 414 {
419 withdrawal_enabled: '是', 415 withdrawal_enabled: '是',
420 withdrawal_mode: '指定提取金额', 416 withdrawal_mode: '指定提取金额',
421 - specified_amount_type: '按年岁', 417 + withdrawal_method: '按年岁',
422 withdrawal_start_age: 60, // 由几岁开始 418 withdrawal_start_age: 60, // 由几岁开始
423 withdrawal_period: '10年', // 提取期(年) 419 withdrawal_period: '10年', // 提取期(年)
424 increase_rate: '5' // 每年递增提取之百分比(%) 420 increase_rate: '5' // 每年递增提取之百分比(%)
425 } 421 }
426 ``` 422 ```
427 423
428 -**按保单年度字段**(2个):
429 -```javascript
430 -{
431 - withdrawal_enabled: '是',
432 - withdrawal_mode: '指定提取金额',
433 - specified_amount_type: '按保单年度',
434 - withdrawal_start_age: 60, // 由几岁开始
435 - withdrawal_period: '10年' // 提取期(年)
436 -}
437 -```
438 -
439 ##### B. 最高固定提取金额模式(2个字段) 424 ##### B. 最高固定提取金额模式(2个字段)
440 425
441 ```javascript 426 ```javascript
...@@ -452,23 +437,22 @@ src/ ...@@ -452,23 +437,22 @@ src/
452 - 无需"每年提取金额"字段(小程序端不需要) 437 - 无需"每年提取金额"字段(小程序端不需要)
453 - 字段清理逻辑:切换模式时自动清除不相关字段 438 - 字段清理逻辑:切换模式时自动清除不相关字段
454 439
455 -**组件设计(三层结构)** 440 +**组件设计(独立字段结构)**
456 441
457 ```vue 442 ```vue
458 <template> 443 <template>
459 <div> 444 <div>
460 - <!-- 第一层:启用确认 --> 445 + <!-- 字段1:是否生成说明(独立字段) -->
461 <PlanFieldRadio 446 <PlanFieldRadio
462 v-model="form.withdrawal_enabled" 447 v-model="form.withdrawal_enabled"
463 label="是否希望生成一份容许减少名义金额的提取说明?" 448 label="是否希望生成一份容许减少名义金额的提取说明?"
464 :options="['是', '否']" 449 :options="['是', '否']"
465 /> 450 />
466 451
467 - <!-- 第二层 + 第三层:仅当选择"是"时显示 --> 452 + <!-- 字段2:款项提取配置(始终显示) -->
468 - <template v-if="form.withdrawal_enabled === '是'">
469 <h3>款项提取(容许减少名义金额)</h3> 453 <h3>款项提取(容许减少名义金额)</h3>
470 454
471 - <!-- 第二层:提取选项 --> 455 + <!-- 提取选项 -->
472 <PlanFieldRadio 456 <PlanFieldRadio
473 v-model="form.withdrawal_mode" 457 v-model="form.withdrawal_mode"
474 label="提取选项" 458 label="提取选项"
...@@ -476,17 +460,17 @@ src/ ...@@ -476,17 +460,17 @@ src/
476 @change="onWithdrawalModeChange" 460 @change="onWithdrawalModeChange"
477 /> 461 />
478 462
479 - <!-- 第三层 A:指定提取金额模式 --> 463 + <!-- 指定提取金额模式 -->
480 <template v-if="form.withdrawal_mode === '指定提取金额'"> 464 <template v-if="form.withdrawal_mode === '指定提取金额'">
481 <!-- 子选项:提取方式 --> 465 <!-- 子选项:提取方式 -->
482 <PlanFieldRadio 466 <PlanFieldRadio
483 - v-model="form.specified_amount_type" 467 + v-model="form.withdrawal_method"
484 label="提取方式" 468 label="提取方式"
485 - :options="['按年岁', '按保单年度']" 469 + :options="['按年岁']"
486 /> 470 />
487 471
488 <!-- 按年岁字段 --> 472 <!-- 按年岁字段 -->
489 - <template v-if="form.specified_amount_type === '按年岁'"> 473 + <template v-if="form.withdrawal_method === '按年岁'">
490 <PlanFieldAgePicker 474 <PlanFieldAgePicker
491 v-model="form.withdrawal_start_age" 475 v-model="form.withdrawal_start_age"
492 label="由几岁开始" 476 label="由几岁开始"
...@@ -512,25 +496,9 @@ src/ ...@@ -512,25 +496,9 @@ src/
512 /> 496 />
513 </div> 497 </div>
514 </template> 498 </template>
515 -
516 - <!-- 按保单年度字段 -->
517 - <template v-if="form.specified_amount_type === '按保单年度'">
518 - <PlanFieldAgePicker
519 - v-model="form.withdrawal_start_age"
520 - label="由几岁开始"
521 - placeholder="请输入开始提取年龄"
522 - />
523 -
524 - <PlanFieldSelect
525 - v-model="form.withdrawal_period"
526 - label="提取期(年)"
527 - placeholder="请选择提取期"
528 - :options="withdrawalPeriods"
529 - />
530 - </template>
531 </template> 499 </template>
532 500
533 - <!-- 第三层 B:最高固定提取金额模式 --> 501 + <!-- 最高固定提取金额模式 -->
534 <template v-if="form.withdrawal_mode === '最高固定提取金额'"> 502 <template v-if="form.withdrawal_mode === '最高固定提取金额'">
535 <PlanFieldAgePicker 503 <PlanFieldAgePicker
536 v-model="form.withdrawal_start_age" 504 v-model="form.withdrawal_start_age"
...@@ -545,7 +513,6 @@ src/ ...@@ -545,7 +513,6 @@ src/
545 :options="withdrawalPeriods" 513 :options="withdrawalPeriods"
546 /> 514 />
547 </template> 515 </template>
548 - </template>
549 </div> 516 </div>
550 </template> 517 </template>
551 518
...@@ -554,29 +521,17 @@ src/ ...@@ -554,29 +521,17 @@ src/
554 const onWithdrawalModeChange = (mode) => { 521 const onWithdrawalModeChange = (mode) => {
555 if (mode === '最高固定提取金额') { 522 if (mode === '最高固定提取金额') {
556 // 最高固定金额模式不需要指定金额的相关字段 523 // 最高固定金额模式不需要指定金额的相关字段
557 - delete form.specified_amount_type 524 + delete form.withdrawal_method
558 delete form.increase_rate 525 delete form.increase_rate
559 } 526 }
560 } 527 }
561 528
562 // 监听提取方式变化 529 // 监听提取方式变化
563 -watch(() => form.specified_amount_type, (newType) => { 530 +watch(() => form.withdrawal_method, (newType) => {
564 // 两种方式都不需要 annual_amount 和 increase_rate(小程序端不需要) 531 // 两种方式都不需要 annual_amount 和 increase_rate(小程序端不需要)
565 delete form.annual_amount 532 delete form.annual_amount
566 delete form.increase_rate 533 delete form.increase_rate
567 }) 534 })
568 -
569 -// 监听启用状态变化
570 -watch(() => form.withdrawal_enabled, (newValue) => {
571 - if (newValue === '否') {
572 - // 清除所有提取计划相关字段
573 - delete form.withdrawal_mode
574 - delete form.specified_amount_type
575 - delete form.withdrawal_start_age
576 - delete form.withdrawal_period
577 - delete form.increase_rate
578 - }
579 -})
580 </script> 535 </script>
581 ``` 536 ```
582 537
......
...@@ -292,7 +292,7 @@ console.log(product.form_sn) // 应该有值,如 "life-insurance-wiop3e" ...@@ -292,7 +292,7 @@ console.log(product.form_sn) // 应该有值,如 "life-insurance-wiop3e"
292 292
293 - 第一层:是否启用(是/否) 293 - 第一层:是否启用(是/否)
294 - 第二层:提取选项(指定提取金额/最高固定提取金额) 294 - 第二层:提取选项(指定提取金额/最高固定提取金额)
295 -- 第三层:具体方式(按年岁/按保单年度 295 +- 第三层:具体方式(按年岁)
296 296
297 确保按顺序选择,相关字段会自动显示。 297 确保按顺序选择,相关字段会自动显示。
298 298
......
1 +# 计划书表单 Schema 使用文档
2 +
3 +## 1. 文档目标
4 +用于说明计划书表单的 Schema 配置规范、字段类型、联动规则与提交映射,便于后续新增或扩展不同保险类型时快速落地。
5 +
6 +## 2. 核心思路
7 +- 统一由 Schema 描述字段渲染、校验与联动
8 +- 统一由 submit_mapping 处理字段到 API 字段的映射与金额转换
9 +- 模板组件只负责“渲染与校验”,不再硬编码字段逻辑
10 +
11 +## 3. Schema 结构
12 +```javascript
13 +// Schema 基础结构
14 +const form_schema = {
15 + // 基础字段
16 + base_fields: [
17 + {
18 + id: 'customer_name',
19 + key: 'customer_name',
20 + type: 'name',
21 + label: '申请人',
22 + placeholder: '请输入申请人',
23 + required: true
24 + }
25 + ],
26 + // 提取计划字段(可选)
27 + withdrawal_fields: [],
28 + // 联动清空规则(可选)
29 + reset_map: {}
30 +}
31 +```
32 +
33 +## 4. 字段类型说明
34 +| type | 组件 | 说明 |
35 +| --- | --- | --- |
36 +| name | NameInput | 姓名输入 |
37 +| radio | RadioGroup | 单选 |
38 +| date | DatePickerGlobal | 日期选择 |
39 +| amount | AmountKeyboard | 金额键盘输入(内部存分) |
40 +| age | AgePickerGlobal | 年龄选择 |
41 +| select | SelectPickerGlobal | 下拉选择 |
42 +| payment_period | PaymentPeriodRadio | 缴费年期 |
43 +| percentage | NutInput | 百分比输入 |
44 +
45 +## 5. 字段属性说明
46 +```javascript
47 +// 字段属性示例
48 +{
49 + id: 'coverage',
50 + key: 'coverage',
51 + type: 'amount',
52 + label: '年缴保费',
53 + placeholder: '请输入年缴保费',
54 + input_label: '请输入年缴保费金额',
55 + required: true,
56 + // 可从配置读取币种
57 + currency_from: 'currency',
58 + // 控制显示条件
59 + show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }],
60 + // 默认值
61 + default: '否',
62 + // 标题分组
63 + section_title: '款项提取(允许减少名义金额)'
64 +}
65 +```
66 +
67 +## 6. 联动规则与清空逻辑
68 +```javascript
69 +// 提取模式切换后,按规则清空脏字段
70 +const reset_map = {
71 + withdrawal_mode: {
72 + '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'],
73 + '指定提取金额': ['withdrawal_start_age', 'withdrawal_period']
74 + }
75 +}
76 +```
77 +
78 +## 7. 提交字段映射
79 +```javascript
80 +// submit_mapping 示例(金额字段统一从分转元)
81 +const submit_mapping = {
82 + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
83 + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
84 + withdrawal_mode: { api_field: 'withdrawal_option' }
85 +}
86 +```
87 +
88 +## 8. 使用示例
89 +```vue
90 +<!-- 储蓄型模板使用示例 -->
91 +<template>
92 + <SavingsTemplate v-model="form_data" :config="template_config" />
93 +</template>
94 +
95 +<script setup>
96 +// 表单数据
97 +const form_data = ref({})
98 +
99 +// 模板配置(通常来自 plan-templates.js)
100 +const template_config = {
101 + currency: 'USD',
102 + payment_periods: ['整付', '5 年'],
103 + withdrawal_plan: {
104 + enabled: true,
105 + default_currency: 'USD',
106 + withdrawal_periods: ['1年', '2年', '终身']
107 + },
108 + form_schema: {},
109 + submit_mapping: {}
110 +}
111 +</script>
112 +```
113 +
114 +## 8.1 人寿/重疾模板使用示例
115 +```vue
116 +<template>
117 + <LifeInsuranceTemplate v-model="form_data" :config="template_config" />
118 +</template>
119 +
120 +<script setup>
121 +const form_data = ref({})
122 +
123 +const template_config = {
124 + currency: 'USD',
125 + payment_periods: ['整付(0-75 岁)', '5 年(0-70 岁)'],
126 + form_schema: protectionFormSchema,
127 + submit_mapping: baseSubmitMapping
128 +}
129 +</script>
130 +```
131 +
132 +## 9. 新增保险类型流程
133 +1.`src/config/plan-templates.js` 新增产品项(配置 form_sn)
134 +2. 为该产品选择已有模板组件或新增模板组件
135 +3. 定义 `form_schema``submit_mapping`
136 +4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑)
137 +5. 验证校验与提交映射
138 +
139 +## 10. 新增产品配置示例
140 +```javascript
141 +// 示例:新增储蓄类产品配置
142 +'savings-new': {
143 + name: '示例储蓄产品',
144 + component: 'SavingsTemplate',
145 + category: 'savings',
146 + config: {
147 + currency: 'USD',
148 + payment_periods: ['整付', '5 年'],
149 + withdrawal_plan: {
150 + enabled: true,
151 + default_currency: 'USD',
152 + withdrawal_periods: ['1年', '2年', '终身']
153 + },
154 + form_schema: savingsFormSchema,
155 + submit_mapping: savingsSubmitMapping
156 + }
157 +}
158 +```
159 +
160 +```javascript
161 +// 示例:新增人寿/重疾类产品配置
162 +'life-insurance-new': {
163 + name: '示例人寿产品',
164 + component: 'LifeInsuranceTemplate',
165 + config: {
166 + currency: 'USD',
167 + payment_periods: ['整付(0-75 岁)'],
168 + form_schema: protectionFormSchema,
169 + submit_mapping: baseSubmitMapping
170 + }
171 +}
172 +```
173 +
174 +## 11. 常见扩展点
175 +- 新字段:仅在 form_schema 增加字段并补充 submit_mapping
176 +- 新联动:在 show_when 与 reset_map 中定义条件
177 +- 新模板:复用现有字段组件,保持 schema 结构一致
178 +
179 +## 12. 计划书模块入口与配置地图
180 +### 12.1 页面入口
181 +- 产品详情:`src/pages/product-detail/index.vue`(按钮打开计划书弹窗)
182 +- 产品中心:`src/pages/product-center/index.vue`(列表内“计划书”按钮)
183 +- 搜索页:`src/pages/search/index.vue`(搜索结果卡片“计划书”按钮)
184 +- 计划书列表:`src/pages/plan/index.vue`(查看/删除计划书)
185 +- 提交结果页:`src/pages/plan-submit-result/index.vue`
186 +
187 +### 12.2 组件与模板
188 +- 弹窗容器:`src/components/plan/PlanPopupNew.vue`
189 +- 计划书容器:`src/components/plan/PlanFormContainer.vue`
190 +- 模板组件:
191 + - `src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue`
192 + - `src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue`
193 + - `src/components/plan/PlanTemplates/SavingsTemplate.vue`
194 +- 字段组件:`src/components/plan/PlanFields/*`
195 +
196 +### 12.3 配置与数据处理
197 +- 模板映射:`src/config/plan-templates.js`
198 +- 字段定义与映射:`src/config/plan-fields.js`
199 +- 字段转换函数:`src/utils/planFieldTransformers.js`
200 +- 字段转换入口:`src/composables/useFieldValueTransform.js`
201 +- 字段联动规则:`src/composables/useFieldDependencies.js`
202 +- 字段校验工具:`src/utils/planFieldValidation.js`
203 +- 订单状态常量:`src/config/constants/orderStatus.js`
204 +
205 +### 12.4 API 入口
206 +- 计划书 API:`src/api/plan.js`
207 + - 新增:`addAPI`
208 + - 列表:`listAPI`
209 + - 删除:`deleteAPI`
210 + - 查看:`viewAPI`
211 +
212 +### 12.5 技术书/附件预览关联
213 +- 产品详情附件列表:`src/pages/product-detail/index.vue`
214 +- 文件预览能力:`src/composables/useFileOperation.js`
215 +
216 +## 13. 计划书模块使用流程
217 +1. 产品详情/产品中心/搜索页获取产品对象(至少包含 `id``form_sn`,可选 `plan_config`
218 +2. 打开 `PlanFormContainer` 并传入 `product`
219 +3. `PlanFormContainer` 根据 `form_sn``plan-templates` 选择模板并合并 `plan_config`
220 +4. 模板组件基于 `form_schema` 渲染字段,调用自身 `validate` 完成校验
221 +5. 提交时使用 `submit_mapping` 生成请求参数,并通过 `addAPI` 提交
222 +6. 提交完成后通过 `usePlanSubmit` 跳转到提交结果页
223 +7. 在计划书列表中用 `listAPI` 拉取数据,使用 `viewAPI` 标记为已查看
224 +
225 +## 14. 计划书容器使用示例
226 +```vue
227 +<template>
228 + <PlanFormContainer
229 + v-model:visible="show_plan_popup"
230 + :product="selected_product"
231 + @close="show_plan_popup = false"
232 + @submit="handle_plan_submit"
233 + />
234 +</template>
235 +
236 +<script setup>
237 +import { ref } from 'vue'
238 +import PlanFormContainer from '@/components/plan/PlanFormContainer.vue'
239 +import { usePlanSubmit } from '@/composables/usePlanSubmit'
240 +
241 +const show_plan_popup = ref(false)
242 +const selected_product = ref(null)
243 +
244 +const { handlePlanSubmit: handle_plan_submit } = usePlanSubmit({
245 + getPopupState: () => show_plan_popup.value,
246 + setPopupState: (state) => { show_plan_popup.value = state },
247 + pageName: 'Plan Entry'
248 +})
249 +</script>
250 +```
1 # 臻奇智荟圈小程序 - 前端开发计划(调整版) 1 # 臻奇智荟圈小程序 - 前端开发计划(调整版)
2 2
3 +## ⚠️ 当前说明
4 +
5 +本计划为历史版本,当前业务与路由以 `src/app.config.js` 为准,AI 模块已改为外部配置,不再内置页面。
6 +
3 ## 📋 项目概览 7 ## 📋 项目概览
4 8
5 ### 项目信息 9 ### 项目信息
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 - **设计宽度**: 750px(自定义组件)/ 375px(NutUI组件) 9 - **设计宽度**: 750px(自定义组件)/ 375px(NutUI组件)
10 10
11 ### 开发目标 11 ### 开发目标
12 -基于现有 Taro 4 + Vue 3 模板,开发服务保险团队内部同事的微信小程序,实现计划书生成、资料库管理、AI问答三大核心功能 12 +基于现有 Taro 4 + Vue 3 模板,开发服务保险团队内部同事的微信小程序,实现产品与资料浏览、计划书管理、搜索与消息通知、反馈闭环等核心能力
13 13
14 --- 14 ---
15 15
...@@ -19,65 +19,46 @@ ...@@ -19,65 +19,46 @@
19 19
20 ``` 20 ```
21 src/ 21 src/
22 -├── api/ # API接口定义 22 +├── api/ # API 接口层
23 -│ ├── index.js # API入口
24 -│ ├── fn.js # 请求包装器
25 -│ ├── order.js # 订单相关API
26 -│ ├── material.js # 资料库相关API
27 -│ ├── ai.js # AI问答相关API
28 -│ └── user.js # 用户相关API
29 -
30 ├── assets/ # 静态资源 23 ├── assets/ # 静态资源
31 -│ └── images/ # 图片资源 24 +├── components/ # 通用组件
32 - 25 +│ ├── cards/ # 卡片组件
33 -├── components/ # 公共组件 26 +│ ├── documents/ # 文档预览组件
34 -│ ├── page-container/ # 页面容器组件 27 +│ ├── forms/ # 表单组件
35 -│ ├── order-card/ # 订单卡片组件 28 +│ ├── icons/ # 图标组件
36 -│ ├── material-item/ # 资料列表项组件 29 +│ ├── list/ # 列表组件
37 -│ ├── file-preview/ # 文件预览组件(PDF/视频/图片) 30 +│ ├── navigation/ # 导航组件
38 -│ └── chat-message/ # 聊天消息组件 31 +│ └── plan/ # 计划书相关组件
39 -
40 ├── composables/ # 组合式函数 32 ├── composables/ # 组合式函数
41 -│ ├── useAuth.js # 认证相关 33 +├── config/ # 功能与权限配置
42 -│ ├── useRequest.js # 请求封装 34 +├── hooks/ # hooks
43 -│ └── useUpload.js # 文件上传 35 +├── pages/ # 页面组件
44 - 36 +│ ├── index/ # 首页
45 -├── pages/ # 页面 37 +│ ├── product-center/ # 产品中心
46 -│ ├── index/ # 首页(工作台) 38 +│ ├── product-detail/ # 产品详情
47 -│ ├── order/ 39 +│ ├── category-list/ # 分类列表
48 -│ │ ├── submit/ # 提交计划书申请 40 +│ ├── material-list/ # 资料列表
49 -│ │ ├── list/ # 我的订单列表 41 +│ ├── week-hot-material/ # 周热门资料
50 -│ │ └── detail/ # 订单详情 42 +│ ├── signing/ # 签单相关
51 -│ ├── material/ 43 +│ ├── family-office/ # 家办业务
52 -│ │ ├── index/ # 资料库首页 44 +│ ├── plan/ # 计划书列表
53 -│ │ ├── list/ # 资料列表 45 +│ ├── plan-submit-result/ # 计划书提交结果
54 -│ │ ├── detail/ # 资料详情(PDF/视频预览) 46 +│ ├── search/ # 搜索
55 -│ │ └── search/ # 资料搜索 47 +│ ├── document-preview/ # 文档预览
56 -│ ├── ai/ 48 +│ ├── document-demo/ # 文档演示
57 -│ │ └── chat/ # AI问答对话页面 49 +│ ├── message/ # 消息列表
58 -│ ├── notifications/ # 消息通知列表 50 +│ ├── message-detail/ # 消息详情
59 -│ ├── profile/ # 个人中心 51 +│ ├── feedback/ # 意见反馈
60 -│ └── auth/ # 授权登录(已有) 52 +│ ├── feedback-list/ # 反馈列表
61 - 53 +│ ├── favorites/ # 我的收藏
62 -├── stores/ # 状态管理 54 +│ ├── mine/ # 我的
63 -│ ├── main.js # 主Store 55 +│ ├── avatar/ # 头像编辑
64 -│ ├── router.js # 路由Store(已有) 56 +│ ├── help-center/ # 帮助中心
65 -│ ├── user.js # 用户信息Store 57 +│ ├── login/ # 登录
66 -│ ├── order.js # 订单Store 58 +│ ├── onboarding/ # 引导页
67 -│ └── material.js # 资料库Store 59 +│ ├── video-player/ # 视频播放
68 - 60 +│ └── webview/ # WebView 承载页
69 -├── utils/ # 工具函数 61 +├── app.config.js # 页面路由配置
70 -│ ├── authRedirect.js # 认证跳转(已有)
71 -│ ├── request.js # 请求封装(已有)
72 -│ ├── tools.js # 工具函数(已有)
73 -│ ├── config.js # 配置文件(已有)
74 -│ ├── validate.js # 表单验证
75 -│ └── format.js # 格式化工具
76 -
77 -├── hooks/ # Hooks
78 -│ └── useGo.js # 导航Hook(已有)
79 -
80 -├── app.config.js # 应用配置(路由、tabBar等)
81 └── app.js # 应用入口 62 └── app.js # 应用入口
82 ``` 63 ```
83 64
...@@ -85,60 +66,63 @@ src/ ...@@ -85,60 +66,63 @@ src/
85 66
86 | 页面路径 | 页面名称 | 需要登录 | 说明 | 67 | 页面路径 | 页面名称 | 需要登录 | 说明 |
87 |---------|---------|---------|------| 68 |---------|---------|---------|------|
88 -| /pages/index/index | 首页/工作台 | ✅ | 展示快捷入口、待处理订单、最新资料 | 69 +| /pages/index/index | 首页 | ✅ | 导航入口、产品与资料推荐 |
89 -| /pages/order/submit | 提交计划书申请 | ✅ | 表单提交页面 | 70 +| /pages/search/index | 搜索 | ✅ | 全局搜索与分类结果 |
90 -| /pages/order/list | 我的订单 | ✅ | 订单列表(按状态筛选) | 71 +| /pages/webview/index | WebView | ✅ | 承载外部 H5 |
91 -| /pages/order/detail | 订单详情 | ✅ | 查看订单详情、PDF预览、海报查看 | 72 +| /pages/document-preview/index | 文档预览 | ✅ | PDF/Office 预览 |
92 -| /pages/material/index | 资料库首页 | ✅ | 分类导航、热门资料 | 73 +| /pages/document-demo/index | 文档演示 | ✅ | 预览演示页面 |
93 -| /pages/material/list | 资料列表 | ✅ | 按分类查看资料 | 74 +| /pages/onboarding/index | 引导页 | ❌ | 首次引导 |
94 -| /pages/material/detail | 资料详情 | ✅ | PDF/视频/图片预览(禁止下载) | 75 +| /pages/family-office/index | 家办业务 | ✅ | 家办资料入口 |
95 -| /pages/material/search | 资料搜索 | ✅ | 搜索资料 | 76 +| /pages/product-center/index | 产品中心 | ✅ | 产品聚合与筛选 |
96 -| /pages/ai/chat | AI问答 | ✅ | 对话式AI交互 | 77 +| /pages/product-detail/index | 产品详情 | ✅ | 产品信息与附件 |
97 -| /pages/notifications | 消息通知 | ✅ | 系统消息列表 | 78 +| /pages/category-list/index | 分类列表 | ✅ | 分类聚合列表 |
98 -| /pages/profile | 个人中心 | ✅ | 用户信息、设置 | 79 +| /pages/material-list/index | 资料列表 | ✅ | 分类资料列表 |
99 -| /pages/auth/index | 授权登录 | ❌ | 微信登录(已有) | 80 +| /pages/week-hot-material/index | 周热门资料 | ✅ | 热门资料聚合 |
81 +| /pages/signing/index | 签单相关 | ✅ | 签单资料入口 |
82 +| /pages/mine/index | 我的 | ✅ | 个人入口 |
83 +| /pages/plan/index | 计划书 | ✅ | 计划书列表 |
84 +| /pages/plan-submit-result/index | 计划书提交结果 | ✅ | 提交完成与引导 |
85 +| /pages/favorites/index | 收藏 | ✅ | 收藏管理 |
86 +| /pages/avatar/index | 头像编辑 | ✅ | 头像与信息编辑 |
87 +| /pages/feedback-list/index | 反馈列表 | ✅ | 历史反馈 |
88 +| /pages/feedback/index | 意见反馈 | ✅ | 反馈提交 |
89 +| /pages/login/index | 登录 | ❌ | 登录与回跳 |
90 +| /pages/help-center/index | 帮助中心 | ✅ | 常见问题与入口 |
91 +| /pages/message/index | 消息列表 | ✅ | 消息通知 |
92 +| /pages/message-detail/index | 消息详情 | ✅ | 消息详情与计划书状态 |
93 +| /pages/video-player/index | 视频播放 | ✅ | 视频播放页面 |
100 94
101 ### TabBar 配置 95 ### TabBar 配置
102 96
103 -```javascript 97 +当前采用自定义 TabBar 组件(`src/components/navigation/TabBar.vue`),原生 `tabBar` 未启用,路由以 `app.config.js` 为准。
104 -// app.config.js
105 -tabBar: {
106 - color: '#999999',
107 - selectedColor: '#007AFF',
108 - backgroundColor: '#ffffff',
109 - borderStyle: 'black',
110 - list: [
111 - {
112 - pagePath: 'pages/index/index',
113 - text: '工作台',
114 - iconPath: 'assets/images/tab-home.png',
115 - selectedIconPath: 'assets/images/tab-home-active.png'
116 - },
117 - {
118 - pagePath: 'pages/material/index/index',
119 - text: '资料库',
120 - iconPath: 'assets/images/tab-material.png',
121 - selectedIconPath: 'assets/images/tab-material-active.png'
122 - },
123 - {
124 - pagePath: 'pages/ai/chat/index',
125 - text: 'AI助手',
126 - iconPath: 'assets/images/tab-ai.png',
127 - selectedIconPath: 'assets/images/tab-ai-active.png'
128 - },
129 - {
130 - pagePath: 'pages/profile/index',
131 - text: '我的',
132 - iconPath: 'assets/images/tab-profile.png',
133 - selectedIconPath: 'assets/images/tab-profile-active.png'
134 - }
135 - ]
136 -}
137 -```
138 98
139 --- 99 ---
140 100
141 -## 📱 核心功能模块设计 101 +## ✅ 当前功能模块概览
102 +
103 +### 模块1:产品与资料
104 +- 产品中心、产品详情、分类列表、资料列表、周热门资料、签单相关、家办业务
105 +- 文档预览与视频播放作为统一内容承载页面
106 +
107 +### 模块2:计划书流程
108 +- 计划书列表与状态展示
109 +- 提交结果页与消息详情联动
110 +
111 +### 模块3:搜索与消息
112 +- 搜索结果统一入口
113 +- 消息列表与详情承载计划书状态更新
114 +
115 +### 模块4:个人中心与反馈
116 +- 我的、收藏、头像、帮助中心
117 +- 反馈提交与反馈历史列表
118 +
119 +---
120 +
121 +---
122 +
123 +## 🗃️ 历史规划(已停用)
124 +
125 +以下内容为历史规划记录,已与当前业务实现不一致,阅读时请以“当前功能模块概览”和 `app.config.js` 为准。
142 126
143 ### 模块1:计划书生成模块 127 ### 模块1:计划书生成模块
144 128
......
...@@ -12,18 +12,31 @@ ...@@ -12,18 +12,31 @@
12 12
13 ### 项目定位 13 ### 项目定位
14 服务保险团队内部同事的轻量化微信小程序,核心解决三大痛点: 14 服务保险团队内部同事的轻量化微信小程序,核心解决三大痛点:
15 -1. 计划书快速生成+状态实时反馈 15 +1. 计划书管理与状态实时反馈
16 -2. 沉淀内部培训、服务资料,打造专属知识库 16 +2. 产品与资料沉淀、统一检索与消息通知
17 -3. AI智能问答功能 17 +3. 反馈闭环与个人中心能力
18 18
19 ### 核心技术决策 19 ### 核心技术决策
20 - **不对接保险公司官方API**:规避高成本、高门槛问题 20 - **不对接保险公司官方API**:规避高成本、高门槛问题
21 - **采用半人工方式**:前端提交+后台人工协同的低成本落地方案 21 - **采用半人工方式**:前端提交+后台人工协同的低成本落地方案
22 -- **AI功能**:采用腾讯元宝AI,建立团队私有的知识库 22 +- **AI能力**:采用腾讯元宝AI进行外部配置,不在小程序内置页面
23 23
24 --- 24 ---
25 25
26 -## 🎯 需求分析 26 +## ✅ 当前业务概览
27 +
28 +### 核心模块
29 +1. 产品与资料:产品中心、资料分类、周热门、签单与家办入口
30 +2. 计划书:计划书列表、提交结果与消息联动
31 +3. 搜索与消息:全局搜索、消息列表与详情
32 +4. 个人中心与反馈:我的、收藏、头像、帮助中心、意见反馈
33 +
34 +### 当前路由基准
35 +`src/app.config.js` 为准,涉及页面包含首页、搜索、文档预览、文档演示、产品中心、计划书、消息、反馈、登录等。
36 +
37 +---
38 +
39 +## 🗃️ 历史需求分析(已停用)
27 40
28 ### 一、核心功能模块 41 ### 一、核心功能模块
29 42
...@@ -108,7 +121,7 @@ ...@@ -108,7 +121,7 @@
108 | 数据库 | 存储订单、用户、资料数据 | MySQL / PostgreSQL | 121 | 数据库 | 存储订单、用户、资料数据 | MySQL / PostgreSQL |
109 | 文件存储 | PDF、培训资料、海报、视频 | 七牛云私有云存储 | 122 | 文件存储 | PDF、培训资料、海报、视频 | 七牛云私有云存储 |
110 | CDN加速 | 视频、图片加速 | 七牛云CDN | 123 | CDN加速 | 视频、图片加速 | 七牛云CDN |
111 -| AI服务 | 智能问答 | 腾讯元宝AI | 124 +| AI服务 | 外部知识库配置 | 腾讯元宝AI(外部配置) |
112 | 即时通讯 | 消息推送 | 微信小程序订阅消息 | 125 | 即时通讯 | 消息推送 | 微信小程序订阅消息 |
113 126
114 ### 系统架构图 127 ### 系统架构图
...@@ -117,7 +130,7 @@ ...@@ -117,7 +130,7 @@
117 ┌─────────────────────────────────────────────────────────────┐ 130 ┌─────────────────────────────────────────────────────────────┐
118 │ 微信小程序前端 │ 131 │ 微信小程序前端 │
119 │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ 132 │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
120 -│ │计划书生成 │ │ 资料库 │ │ AI问答 │ │ 个人中心 │ │ 133 +│ │计划书流程 │ │ 资料中心 │ │ 搜索消息 │ │ 个人中心 │ │
121 │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ 134 │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
122 └─────────────────────────────────────────────────────────────┘ 135 └─────────────────────────────────────────────────────────────┘
123 ↕ HTTPS 136 ↕ HTTPS
...@@ -252,7 +265,7 @@ CREATE TABLE operation_logs ( ...@@ -252,7 +265,7 @@ CREATE TABLE operation_logs (
252 265
253 --- 266 ---
254 267
255 -## 📅 开发计划与里程碑 268 +## 🗓️ 历史开发计划与里程碑(已停用)
256 269
257 ### 总体时间规划 270 ### 总体时间规划
258 - **项目启动**: 2026-01-20 271 - **项目启动**: 2026-01-20
......
...@@ -83,10 +83,10 @@ docs/ ...@@ -83,10 +83,10 @@ docs/
83 ### 核心文档 83 ### 核心文档
84 - 📖 [项目变更日志](CHANGELOG.md) - 所有功能、修复和优化的记录 84 - 📖 [项目变更日志](CHANGELOG.md) - 所有功能、修复和优化的记录
85 - 📖 [经验教训总结](lessons-learned.md) - 开发中的最佳实践和常见陷阱 85 - 📖 [经验教训总结](lessons-learned.md) - 开发中的最佳实践和常见陷阱
86 -- 📖 [API 联调日志](api-specs/API 集成日志.md) - 接口联调状态记录 86 +- 📖 [API 联调日志](api-docs/API 集成日志.md) - 接口联调状态记录
87 87
88 ### 新手入门 88 ### 新手入门
89 -👉 **[guides/新人入门指南.md](guides/新人入门指南.md)** - 快速了解项目功能 89 +👉 **[guides/新人入门指南.md](guides/新人入门指南.md)** - 快速了解业务与页面结构
90 90
91 ### 开发指南 91 ### 开发指南
92 - 📘 [Taro 开发速查表](guides/Taro 开发速查表.md) - Taro API 快速查阅 92 - 📘 [Taro 开发速查表](guides/Taro 开发速查表.md) - Taro API 快速查阅
...@@ -105,6 +105,9 @@ docs/ ...@@ -105,6 +105,9 @@ docs/
105 ### 设计文档 105 ### 设计文档
106 - 🎨 [UI/UX 设计稿](design/manulife-V1/done/) - 各页面设计稿 106 - 🎨 [UI/UX 设计稿](design/manulife-V1/done/) - 各页面设计稿
107 107
108 +### 业务规划
109 +- 📋 [项目开发计划](plan/项目开发计划.md) - 业务规划与功能范围
110 +
108 ## 📖 文档分类说明 111 ## 📖 文档分类说明
109 112
110 ### 📘 guides/ - 使用指南 113 ### 📘 guides/ - 使用指南
...@@ -173,4 +176,4 @@ UI/UX 设计稿和生成的代码: ...@@ -173,4 +176,4 @@ UI/UX 设计稿和生成的代码:
173 176
174 --- 177 ---
175 178
176 -**最后更新**: 2026-02-05 179 +**最后更新**: 2026-02-14
......
This diff could not be displayed because it is too large.
1 -# 🎉 OpenAPI 转 API 文档生成器 - 完成报告 1 +# 新人入门指南
2 - 2 +
3 -## ✅ 已完成的工作 3 +## 项目概览
4 - 4 +
5 -### 1. 核心功能实现 5 +Manulife WeApp(臻奇智荟圈)是面向内部同事的财富管理小程序,核心围绕产品信息、资料内容与计划书流程展开,支持文档预览、消息通知与反馈闭环。
6 -- ✅ 自动化生成器脚本(`scripts/generateApiFromOpenAPI.js` 6 +
7 -- ✅ YAML 解析和验证 7 +## 业务模块
8 -- ✅ 命名转换(驼峰命名、帕斯卡命名) 8 +
9 -- ✅ 模块化组织生成 9 +- **产品与资料**:产品中心、产品详情、分类列表、资料列表、周热门资料
10 -- ✅ 测试验证脚本 10 +- **业务场景**:签单相关、家办业务
11 - 11 +- **计划书**:计划书列表、提交结果页
12 -### 2. 示例和文档 12 +- **内容检索**:搜索页面统一入口
13 -- ✅ 3个 OpenAPI 文档示例(user、order 模块) 13 +- **消息与反馈**:消息列表/详情、反馈提交/历史
14 -- ✅ 2个生成的 API 文件 14 +- **个人中心**:我的、头像、帮助中心、收藏、登录/引导页
15 -- ✅ 完整的使用文档(4份指南) 15 +
16 -- ✅ 演示页面(可直接访问查看效果) 16 +## 页面清单(与路由一致)
17 - 17 +
18 -### 3. 项目集成 18 +1. 首页:`pages/index/index`
19 -- ✅ 添加到 `package.json` 的 npm 命令 19 +2. 搜索:`pages/search/index`
20 -- ✅ 添加路由配置 20 +3. WebView:`pages/webview/index`
21 -- ✅ 安装所需依赖(js-yaml) 21 +4. 文档预览:`pages/document-preview/index`
22 - 22 +5. 文档演示:`pages/document-demo/index`
23 -## 🚀 立即开始使用 23 +6. 引导页:`pages/onboarding/index`
24 - 24 +7. 家办业务:`pages/family-office/index`
25 -### 方式 1: 使用现有示例 25 +8. 产品中心:`pages/product-center/index`
26 +9. 产品详情:`pages/product-detail/index`
27 +10. 分类列表:`pages/category-list/index`
28 +11. 资料列表:`pages/material-list/index`
29 +12. 周热门资料:`pages/week-hot-material/index`
30 +13. 签单相关:`pages/signing/index`
31 +14. 我的:`pages/mine/index`
32 +15. 计划书:`pages/plan/index`
33 +16. 计划书提交结果:`pages/plan-submit-result/index`
34 +17. 收藏:`pages/favorites/index`
35 +18. 头像编辑:`pages/avatar/index`
36 +19. 反馈列表:`pages/feedback-list/index`
37 +20. 意见反馈:`pages/feedback/index`
38 +21. 登录:`pages/login/index`
39 +22. 帮助中心:`pages/help-center/index`
40 +23. 消息列表:`pages/message/index`
41 +24. 消息详情:`pages/message-detail/index`
42 +25. 视频播放:`pages/video-player/index`
43 +
44 +## 本地开发
26 45
27 ```bash 46 ```bash
28 -# 1. 查看生成的 API 文件 47 +pnpm install
29 -cat src/api/user.js
30 -cat src/api/order.js
31 -
32 -# 2. 启动开发服务器
33 pnpm dev:weapp 48 pnpm dev:weapp
34 -
35 -# 3. 访问演示页面
36 -# 路径: pages/examples/api-demo/index
37 -```
38 -
39 -### 方式 2: 创建新的 API
40 -
41 -```bash
42 -# 1. 创建新模块
43 -mkdir -p docs/api-specs/product
44 -
45 -# 2. 创建接口文档
46 -# 复制 docs/api-specs/user/getUserInfo.md 作为模板
47 -# 修改其中的接口信息
48 -
49 -# 3. 生成 API 文件
50 -pnpm api:generate
51 -
52 -# 4. 查看生成的文件
53 -cat src/api/product.js
54 -
55 -# 5. 在项目中使用
56 -import { yourApiAPI } from '@/api/product';
57 ``` 49 ```
58 50
59 -## 📚 文档导航 51 +## 目录速览
60 -
61 -### 快速开始
62 -👉 **[README_API_GENERATOR.md](../README_API_GENERATOR.md)** - 项目总览和快速开始
63 -
64 -### 详细指南
65 -👉 **[QUICKSTART.md](../scripts/QUICKSTART.md)** - 5分钟快速上手
66 -👉 **[OPENAPI_TO_API_GUIDE.md](./OPENAPI_TO_API_GUIDE.md)** - 完整功能说明
67 -👉 **[API_USAGE_EXAMPLES.md](./API_USAGE_EXAMPLES.md)** - 实际使用案例
68 -
69 -### 技术文档
70 -👉 **[IMPLEMENTATION_SUMMARY.md](./IMPLEMENTATION_SUMMARY.md)** - 技术实现细节
71 -
72 -## 🎯 核心命令
73 52
74 -```bash 53 +- `src/pages/`:业务页面
75 -# 生成 API 文件 54 +- `src/components/`:通用组件(文档预览、列表、导航、计划书组件等)
76 -pnpm api:generate 55 +- `src/composables/`:组合式逻辑(权限、文件操作、列表、埋点等)
56 +- `src/api/`:接口封装
57 +- `docs/`:项目文档与流程说明
77 58
78 -# 测试生成的文件 59 +## 常用文档入口
79 -node scripts/test-generate.js
80 -
81 -# 查看帮助
82 -# 查看各文档文件
83 -```
84 60
85 -## 📊 当前状态 61 +- [文档导航](../README.md)
62 +- [API 联调日志](../api-docs/API%20%E9%9B%86%E6%88%90%E6%97%A5%E5%BF%97.md)
63 +- [经验教训总结](../lessons-learned.md)
64 +- [DocumentPreview 组件文档](../../src/components/documents/DocumentPreview/README.md)
86 65
87 -### 已测试的功能 66 +## 常见任务
88 -- ✅ 单接口生成(user/getUserInfo)
89 -- ✅ 批量接口生成(order/getList, order/getDetail)
90 -- ✅ 多模块生成(user、order 两个模块)
91 -- ✅ 文件格式验证
92 -- ✅ 命名转换验证
93 67
94 -### 生成的文件统计 68 +### 新增页面
95 -- **脚本**: 3个(生成器、测试、快速开始)
96 -- **文档**: 4个(指南、示例、总结)
97 -- **OpenAPI 示例**: 3个
98 -- **生成的 API**: 2个模块
99 -- **演示页面**: 1个
100 69
101 -## 💡 使用建议 70 +1.`src/pages/` 新增页面目录与 `index.vue`
102 - 71 +2.`src/app.config.js` 添加路由
103 -### 1. 日常开发流程 72 +3. 如果需要复用布局,优先复用 `NavHeader``TabBar`
104 -```
105 -修改接口 → 更新 OpenAPI 文档 → 运行生成命令 → 使用新 API
106 -```
107 73
108 -### 2. 团队协作 74 +### 联调接口
109 -- 将 OpenAPI 文档作为单一数据源
110 -- 定期运行 `pnpm api:generate` 同步
111 -- 将生成的 API 文件提交到 Git
112 -
113 -### 3. 版本管理
114 -- OpenAPI 文档应该纳入版本控制
115 -- 生成的 API 文件也应该提交
116 -- 确保文档和代码同步更新
117 -
118 -## 🔧 自定义和扩展
119 -
120 -### 修改生成规则
121 -编辑 `scripts/generateApiFromOpenAPI.js`:
122 -
123 -```javascript
124 -// 修改命名规则
125 -function toCamelCase(str) { /* 你的规则 */ }
126 -
127 -// 修改生成模板
128 -function generateApiFileContent(moduleName, apis) { /* 你的模板 */ }
129 -```
130 -
131 -### 添加新功能
132 -- TypeScript 类型定义生成
133 -- Mock 数据生成
134 -- Watch 模式自动重新生成
135 -- 可视化配置界面
136 -
137 -## 📞 遇到问题?
138 -
139 -### 常见问题
140 -1. **生成失败** → 检查 YAML 格式是否正确
141 -2. **导入错误** → 确认文件路径是否正确
142 -3. **命名不符合预期** → 修改 OpenAPI 文档文件名
143 -
144 -### 调试技巧
145 -```bash
146 -# 运行测试脚本
147 -node scripts/test-generate.js
148 -
149 -# 查看生成的文件
150 -cat src/api/your-module.js
151 -
152 -# 查看错误日志
153 -pnpm api:generate
154 -```
155 -
156 -## 🎉 下一步
157 -
158 -### 推荐学习路径
159 -1. **了解概览** → 阅读 README_API_GENERATOR.md
160 -2. **快速上手** → 跟随 QUICKSTART.md 操作
161 -3. **深入学习** → 查看 OPENAPI_TO_API_GUIDE.md
162 -4. **实践应用** → 参考 API_USAGE_EXAMPLES.md
163 -
164 -### 实际应用
165 -- 在项目中创建新的 OpenAPI 文档
166 -- 运行生成命令创建 API
167 -- 在页面中使用生成的 API
168 -- 享受自动化的便利!
169 -
170 -## 📦 文件清单
171 -
172 -```
173 -✅ scripts/generateApiFromOpenAPI.js - 核心生成器
174 -✅ scripts/test-generate.js - 测试脚本
175 -✅ scripts/QUICKSTART.md - 快速开始
176 -
177 -✅ docs/api-specs/user/getUserInfo.md - 用户接口示例
178 -✅ docs/api-specs/order/getList.md - 订单列表示例
179 -✅ docs/api-specs/order/getDetail.md - 订单详情示例
180 -
181 -✅ docs/OPENAPI_TO_API_GUIDE.md - 详细指南
182 -✅ docs/API_USAGE_EXAMPLES.md - 使用示例
183 -✅ docs/IMPLEMENTATION_SUMMARY.md - 实现总结
184 -
185 -✅ src/api/user.js - 用户 API(生成)
186 -✅ src/api/order.js - 订单 API(生成)
187 -
188 -✅ src/pages/examples/api-demo/index.vue - 演示页面
189 -
190 -✅ package.json - 已添加 api:generate 命令
191 -✅ src/app.config.js - 已添加演示页面路由
192 -```
193 75
194 -## 🌟 总结 76 +1.`docs/api-specs/` 更新接口文档
77 +2.`docs/api-docs/API 集成日志.md` 记录联调状态
78 +3.`src/api/` 添加对应接口封装
195 79
196 -你现在拥有一个完整的 OpenAPI 转 API 文档生成器! 80 +## 新人第一天建议
197 81
198 -**核心价值** 82 +1. 先通读 [README](../../README.md) 与本指南
199 --**提高效率** - 自动化生成,节省时间 83 +2. 阅读页面路由与业务模块对照表
200 --**减少错误** - 避免手动编写的不一致 84 +3. 打开首页、产品中心、计划书、消息模块熟悉主流程
201 - 📦 **标准化** - 统一的代码格式 85 - 📦 **标准化** - 统一的代码格式
202 - 🔧 **易维护** - 单一数据源,易于更新 86 - 🔧 **易维护** - 单一数据源,易于更新
203 87
......
...@@ -102,24 +102,24 @@ pnpm dev:weapp ...@@ -102,24 +102,24 @@ pnpm dev:weapp
102 - [ ] 原始请求自动重放成功 102 - [ ] 原始请求自动重放成功
103 - [ ] 新的 sessionid 已保存 103 - [ ] 新的 sessionid 已保存
104 104
105 -#### 测试点 4: 授权页跳转(降级方案) 105 +#### 测试点 4: 登录页跳转(降级方案)
106 106
107 **操作** 107 **操作**
108 1. 模拟授权失败(修改接口返回错误) 108 1. 模拟授权失败(修改接口返回错误)
109 -2. 观察是否跳转到授权 109 +2. 观察是否跳转到登录
110 110
111 **预期结果** 111 **预期结果**
112 ``` 112 ```
113 ✅ 应该看到以下流程: 113 ✅ 应该看到以下流程:
114 1. 授权失败 114 1. 授权失败
115 2. 保存当前页面路径 115 2. 保存当前页面路径
116 -3. 跳转到 /pages/auth/index 116 +3. 跳转到 /pages/login/index
117 -4. 授权成功后回跳原页面 117 +4. 登录成功后回跳原页面
118 ``` 118 ```
119 119
120 **检查点** 120 **检查点**
121 -- [ ] 正确跳转到授权 121 +- [ ] 正确跳转到登录
122 -- [ ] 授权成功后回跳正确 122 +- [ ] 登录成功后回跳正确
123 - [ ] 路径参数不丢失 123 - [ ] 路径参数不丢失
124 124
125 ## 📊 接口说明 125 ## 📊 接口说明
......
...@@ -18,6 +18,54 @@ ...@@ -18,6 +18,54 @@
18 - [架构设计](#架构设计) 18 - [架构设计](#架构设计)
19 - [跨页面通信](#跨页面通信) ⭐ 新增 19 - [跨页面通信](#跨页面通信) ⭐ 新增
20 - [开发工作流](#开发工作流) ⭐ 新增 20 - [开发工作流](#开发工作流) ⭐ 新增
21 +- ### ⭐ 新增: 开发前先询问是否需要搜索现成方案 ⭐ 2026-02-14
22 + **问题描述**:
23 + - 在添加自动更新版本号功能时,我先自己花了 2 小时实现脚本
24 + - 之后才发现项目中已经有 `standard-version` 包和 `release` 脚本
25 + - 浪费了时间,实际上应该先搜索现成方案
26 +
27 + - **根因**:
28 + - 没有全局视图:不知道项目中已有相关工具
29 + - 没有主动搜索:直接开始实现,没有考虑是否已有现成方案
30 + - 沟有沟通:没有先询问用户是否需要搜索
31 +
32 + - **教训**: ⚠️ **开发新功能前必须先询问是否需要搜索网上现成方案**
33 +
34 + **适用场景**:
35 + - ✅ 任何新功能开发前
36 + - ✅ 遇到问题需要解决方案时
37 + - ✅ 考虑技术选型时
38 +
39 + - **执行流程**:
40 + ```
41 + 用户提出需求
42 +
43 + └─→ 问用户:"这个功能是否需要我先搜索网上现成的方案?"
44 +
45 + 用户选择
46 + ├─ "要" → 我先搜索,找到后推荐
47 + └─ "不用" → 我直接开发
48 +
49 + ```
50 +
51 + - **收益**:
52 + - ✅ 避免重复造轮子
53 + - ✅ 使用成熟的解决方案,质量更高
54 + - ✅ 节省开发时间
55 + - ✅ 学习现成方案的最佳实践
56 +
57 + - **相关文件**:
58 + - `package.json` - 已有 `standard-version@9.5.0` 包
59 + - `scripts/release` - 已有 `pnpm release` 脚本
60 + - `scripts/check-changelog.sh` - CHANGELOG 检查脚本
61 +
62 +- **历史记录**:
63 + - **日期**: 2026-02-14
64 + - **问题**: 自动更新版本号功能
65 + - **浪费**: 约 2 小时
66 + - **发现**: 项目已有 standard-version 包
67 +
68 +---
21 - [Mock 数据环境自动切换](#mock-数据环境自动切换模式) ⭐ 新增 69 - [Mock 数据环境自动切换](#mock-数据环境自动切换模式) ⭐ 新增
22 70
23 --- 71 ---
......
...@@ -115,20 +115,14 @@ withdrawal_mode: '指定提取金额' | '最高固定提取金额' ...@@ -115,20 +115,14 @@ withdrawal_mode: '指定提取金额' | '最高固定提取金额'
115 115
116 // 第三层:根据不同选项显示不同字段 116 // 第三层:根据不同选项显示不同字段
117 if (withdrawal_mode === '指定提取金额') { 117 if (withdrawal_mode === '指定提取金额') {
118 - specified_amount_type: '按年岁' | '按保单年度' 118 + withdrawal_method: '按年岁'
119 119
120 - if (specified_amount_type === '按年岁') { 120 + if (withdrawal_method === '按年岁') {
121 withdrawal_start_age: number // 由几岁开始 121 withdrawal_start_age: number // 由几岁开始
122 withdrawal_period: string // 提取期(年) 122 withdrawal_period: string // 提取期(年)
123 increase_rate: string // 每年递增提取之百分比(%) 123 increase_rate: string // 每年递增提取之百分比(%)
124 // ❌ 不需要:annual_amount(小程序端不需要此字段) 124 // ❌ 不需要:annual_amount(小程序端不需要此字段)
125 } 125 }
126 -
127 - if (specified_amount_type === '按保单年度') {
128 - withdrawal_start_age: number
129 - withdrawal_period: string
130 - // ❌ 不需要:annual_amount, increase_rate
131 - }
132 } 126 }
133 127
134 if (withdrawal_mode === '最高固定提取金额') { 128 if (withdrawal_mode === '最高固定提取金额') {
...@@ -151,7 +145,7 @@ watch( ...@@ -151,7 +145,7 @@ watch(
151 (mode) => { 145 (mode) => {
152 if (mode === '最高固定提取金额') { 146 if (mode === '最高固定提取金额') {
153 // 清除指定金额相关字段 147 // 清除指定金额相关字段
154 - delete form.specified_amount_type 148 + delete form.withdrawal_method
155 delete form.annual_amount 149 delete form.annual_amount
156 delete form.increase_rate 150 delete form.increase_rate
157 } 151 }
...@@ -160,7 +154,7 @@ watch( ...@@ -160,7 +154,7 @@ watch(
160 154
161 // 当切换指定金额类型时 155 // 当切换指定金额类型时
162 watch( 156 watch(
163 - () => form.specified_amount_type, 157 + () => form.withdrawal_method,
164 () => { 158 () => {
165 // 小程序端不需要这些字段 159 // 小程序端不需要这些字段
166 delete form.annual_amount 160 delete form.annual_amount
...@@ -175,7 +169,7 @@ watch( ...@@ -175,7 +169,7 @@ watch(
175 if (enabled === '否') { 169 if (enabled === '否') {
176 // 清除所有提取计划字段 170 // 清除所有提取计划字段
177 delete form.withdrawal_mode 171 delete form.withdrawal_mode
178 - delete form.specified_amount_type 172 + delete form.withdrawal_method
179 delete form.withdrawal_start_age 173 delete form.withdrawal_start_age
180 delete form.withdrawal_period 174 delete form.withdrawal_period
181 delete form.annual_amount 175 delete form.annual_amount
......
...@@ -348,8 +348,6 @@ src/ ...@@ -348,8 +348,6 @@ src/
348 │ └── wechat.js # 已存在:微信授权 API 348 │ └── wechat.js # 已存在:微信授权 API
349 349
350 ├── pages/ 350 ├── pages/
351 -│ ├── auth/
352 -│ │ └── index.vue # 删除:不再需要单独的授权页
353 │ └── login/ 351 │ └── login/
354 │ └── index.vue # 保留:用户登录页(账号密码登录) 352 │ └── index.vue # 保留:用户登录页(账号密码登录)
355 353
...@@ -736,7 +734,7 @@ function App(props) { ...@@ -736,7 +734,7 @@ function App(props) {
736 - 更新 401 响应处理 734 - 更新 401 响应处理
737 - [ ] 修改 `src/app.js` - 启动时检查登录状态 735 - [ ] 修改 `src/app.js` - 启动时检查登录状态
738 - [ ] 删除 `src/utils/authRedirect.js` - 移除旧的授权逻辑 736 - [ ] 删除 `src/utils/authRedirect.js` - 移除旧的授权逻辑
739 -- [ ] 删除 `src/pages/auth/index.vue` - 不再需要单独的授权页 737 +- [ ] 确认不再需要单独的授权页(当前仅保留登录页)
740 738
741 ### 第 3 步:更新登录页 739 ### 第 3 步:更新登录页
742 740
......
1 +# 计划书模块优化任务清单
2 +
3 +> **创建时间**: 2026-02-14
4 +> **分支**: feature/优化计划书配置
5 +> **预计总时长**: 3-4 小时
6 +
7 +---
8 +
9 +## 📊 总体进度
10 +
11 +- [x] **第 1 步**: 错误处理增强 (30 分钟)
12 +- [x] **第 2 步**: 添加字段分组 (45 分钟)
13 +- [x] **第 3 步**: 循环依赖检测 (30 分钟)
14 +- [x] **第 4 步**: 简化转换逻辑 (60 分钟)
15 +- [x] **第 5 步**: 添加集成测试 (60 分钟)
16 +
17 +---
18 +
19 +## 📝 任务详情
20 +
21 +### 第 1 步:错误处理增强 (30 分钟)
22 +
23 +**目标**: 增强 `usePlanView.js` 的错误处理和边界情况
24 +
25 +**文件**: `src/composables/usePlanView.js`
26 +
27 +**子任务**:
28 +- [x] 添加 proposal.id 空值检查
29 +- [x] 添加 proposalFiles 空数组检查
30 +- [x] 添加 try-catch 错误捕获
31 +- [x] 添加 onError 回调支持
32 +- [x] 添加错误日志记录
33 +- [x] 更新 JSDoc 注释
34 +
35 +**验收标准**:
36 +- [x] 当 proposal.id 为空时显示友好提示
37 +- [x] 当 proposalFiles 为空时显示友好提示
38 +- [x] 所有错误都被正确捕获和记录
39 +- [x] onError 回调正确执行
40 +
41 +---
42 +
43 +### 第 2 步:添加字段分组 (45 分钟)
44 +
45 +**目标**: 为 `plan-fields.js` 添加逻辑分组,提升配置可读性
46 +
47 +**文件**: `src/config/plan-fields.js`
48 +
49 +**子任务**:
50 +- [x] 定义 FIELD_GROUPS 枚举
51 +- [x] 为每个字段添加 group 属性
52 +- [x] 创建 getFieldsByGroup(group) 工具函数
53 +- [x] 更新 JSDoc 注释
54 +- [x] 更新相关测试用例
55 +
56 +**验收标准**:
57 +- [x] 字段正确分组(BASIC/COVERAGE/WITHDRAWAL)
58 +- [x] getFieldsByGroup 函数正常工作
59 +- [x] 测试覆盖新增函数
60 +
61 +---
62 +
63 +### 第 3 步:循环依赖检测 (30 分钟)
64 +
65 +**目标**: 为 `useFieldDependencies.js` 添加循环依赖检测
66 +
67 +**文件**: `src/composables/useFieldDependencies.js`
68 +
69 +**子任务**:
70 +- [x] 实现 detectCircularDeps 函数
71 +- [x] 在 initFieldStates 中调用检测
72 +- [x] 添加开发环境警告日志
73 +- [x] 更新 JSDoc 注释
74 +- [x] 添加测试用例
75 +
76 +**验收标准**:
77 +- [x] 能正确检测到循环依赖
78 +- [x] 检测时在控制台输出清晰的错误信息
79 +- [x] 不影响正常功能的性能
80 +- [x] 测试覆盖循环依赖场景
81 +
82 +---
83 +
84 +### 第 4 步:简化转换逻辑 (60 分钟)
85 +
86 +**目标**: 简化 `useFieldValueTransform.js` 的转换逻辑
87 +
88 +**文件**: `src/composables/useFieldValueTransform.js`
89 +
90 +**子任务**:
91 +- [x] 将 transformFormData 抽取到 planFieldTransformers.js
92 +- [x] 使用策略模式重构 transform 函数
93 +- [x] 减少重复代码
94 +- [x] 更新 JSDoc 注释
95 +- [x] 更新相关测试用例
96 +
97 +**验收标准**:
98 +- [x] 代码行数减少 20% 以上
99 +- [x] 所有现有测试仍然通过
100 +- [x] 新增测试覆盖边界情况
101 +- [x] 转换逻辑更清晰易懂
102 +
103 +---
104 +
105 +### 第 5 步:添加集成测试 (60 分钟)
106 +
107 +**目标**: 添加计划书模块的集成测试
108 +
109 +**文件**: `src/composables/__tests__/usePlanView.integration.test.js`
110 +
111 +**子任务**:
112 +- [x] 创建集成测试文件
113 +- [x] 编写 viewProposal 完整流程测试
114 +- [x] 编写字段依赖关系测试
115 +- [x] 编写字段转换测试
116 +- [x] 编写错误处理测试
117 +- [x] 确保测试覆盖率 > 80%
118 +
119 +**验收标准**:
120 +- [x] 测试覆盖主要用户流程
121 +- [x] 测试覆盖边界情况
122 +- [x] 所有测试通过
123 +- [x] 测试可重复执行
124 +
125 +---
126 +
127 +## 🔍 快速跳转
128 +
129 +- [查看配置文件](./../../../../src/config/plan-fields.js)
130 +- [查看验证系统](./../../../../src/utils/planFieldValidation.js)
131 +- [查看转换系统](./../../../../src/utils/planFieldTransformers.js)
132 +- [查看依赖处理](./../../../../src/composables/useFieldDependencies.js)
133 +- [查看视图组件](./../../../../src/composables/usePlanView.js)
134 +- [查看测试文件](./../../../../src/composables/__tests__/)
135 +
136 +---
137 +
138 +## 📝 备注
139 +
140 +- 每完成一个子任务,就在对应的 [ ] 中打勾 ✓
141 +- 每完成一大步(5个子任务),就在总体进度中打勾 ✓
142 +- 遇到问题时,在对应任务下添加记录
1 +我现在测试一下更改内容
1 { 1 {
2 "name": "manulife-weapp", 2 "name": "manulife-weapp",
3 - "version": "1.0.0", 3 + "version": "1.5.0",
4 "private": true, 4 "private": true,
5 "description": "基于Taro 4 + Vue 3 + NutUI的微信小程序模板", 5 "description": "基于Taro 4 + Vue 3 + NutUI的微信小程序模板",
6 "templateInfo": { 6 "templateInfo": {
...@@ -36,7 +36,8 @@ ...@@ -36,7 +36,8 @@
36 "prepare": "husky", 36 "prepare": "husky",
37 "parse:docs": "node scripts/parse-docs.js", 37 "parse:docs": "node scripts/parse-docs.js",
38 "parse:docs:list": "node scripts/parse-docs.js --list", 38 "parse:docs:list": "node scripts/parse-docs.js --list",
39 - "parse:docs:file": "node scripts/parse-docs.js --file=\"产品说明书.pdf\"" 39 + "parse:docs:file": "node scripts/parse-docs.js --file=\"产品说明书.pdf\"",
40 + "release": "standard-version"
40 }, 41 },
41 "browserslist": [ 42 "browserslist": [
42 "last 3 versions", 43 "last 3 versions",
...@@ -100,14 +101,15 @@ ...@@ -100,14 +101,15 @@
100 "lint-staged": "^16.2.7", 101 "lint-staged": "^16.2.7",
101 "postcss": "^8.5.6", 102 "postcss": "^8.5.6",
102 "sass": "^1.78.0", 103 "sass": "^1.78.0",
104 + "standard-version": "^9.5.0",
103 "style-loader": "1.3.0", 105 "style-loader": "1.3.0",
104 "tailwindcss": "^3.4.0", 106 "tailwindcss": "^3.4.0",
105 "unplugin-vue-components": "^0.26.0", 107 "unplugin-vue-components": "^0.26.0",
108 + "vitest": "^1.6.0",
106 "vue-eslint-parser": "^9.0.0", 109 "vue-eslint-parser": "^9.0.0",
107 "vue-loader": "^17.0.0", 110 "vue-loader": "^17.0.0",
108 "weapp-tailwindcss": "^4.1.10", 111 "weapp-tailwindcss": "^4.1.10",
109 - "webpack": "5.91.0", 112 + "webpack": "5.91.0"
110 - "vitest": "^1.6.0"
111 }, 113 },
112 "pnpm": { 114 "pnpm": {
113 "onlyBuiltDependencies": [ 115 "onlyBuiltDependencies": [
......
...@@ -165,6 +165,9 @@ importers: ...@@ -165,6 +165,9 @@ importers:
165 sass: 165 sass:
166 specifier: ^1.78.0 166 specifier: ^1.78.0
167 version: 1.97.3 167 version: 1.97.3
168 + standard-version:
169 + specifier: ^9.5.0
170 + version: 9.5.0
168 style-loader: 171 style-loader:
169 specifier: 1.3.0 172 specifier: 1.3.0
170 version: 1.3.0(webpack@5.91.0(@swc/core@1.3.96)) 173 version: 1.3.0(webpack@5.91.0(@swc/core@1.3.96))
...@@ -1425,6 +1428,10 @@ packages: ...@@ -1425,6 +1428,10 @@ packages:
1425 resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} 1428 resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
1426 deprecated: Use @eslint/object-schema instead 1429 deprecated: Use @eslint/object-schema instead
1427 1430
1431 + '@hutson/parse-repository-url@3.0.2':
1432 + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==}
1433 + engines: {node: '>=6.9.0'}
1434 +
1428 '@inquirer/external-editor@1.0.3': 1435 '@inquirer/external-editor@1.0.3':
1429 resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} 1436 resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
1430 engines: {node: '>=18'} 1437 engines: {node: '>=18'}
...@@ -2296,6 +2303,9 @@ packages: ...@@ -2296,6 +2303,9 @@ packages:
2296 resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} 2303 resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
2297 deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. 2304 deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
2298 2305
2306 + '@types/minimist@1.2.5':
2307 + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
2308 +
2299 '@types/ms@2.1.0': 2309 '@types/ms@2.1.0':
2300 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} 2310 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
2301 2311
...@@ -2308,6 +2318,9 @@ packages: ...@@ -2308,6 +2318,9 @@ packages:
2308 '@types/node@25.2.2': 2318 '@types/node@25.2.2':
2309 resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==} 2319 resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==}
2310 2320
2321 + '@types/normalize-package-data@2.4.4':
2322 + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
2323 +
2311 '@types/postcss-url@10.0.4': 2324 '@types/postcss-url@10.0.4':
2312 resolution: {integrity: sha512-5QIO9NgbWmAkle65haRqkdgYPCOXheNsaFdbTJJQjT302yK3H49ql4t9a4y0NfpuPtU/UBo15VcV64WCSIMJKg==} 2325 resolution: {integrity: sha512-5QIO9NgbWmAkle65haRqkdgYPCOXheNsaFdbTJJQjT302yK3H49ql4t9a4y0NfpuPtU/UBo15VcV64WCSIMJKg==}
2313 2326
...@@ -2574,6 +2587,10 @@ packages: ...@@ -2574,6 +2587,10 @@ packages:
2574 '@xtuc/long@4.2.2': 2587 '@xtuc/long@4.2.2':
2575 resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 2588 resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
2576 2589
2590 + JSONStream@1.3.5:
2591 + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
2592 + hasBin: true
2593 +
2577 abbrev@2.0.0: 2594 abbrev@2.0.0:
2578 resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} 2595 resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
2579 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 2596 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
...@@ -2605,6 +2622,9 @@ packages: ...@@ -2605,6 +2622,9 @@ packages:
2605 engines: {node: '>=0.4.0'} 2622 engines: {node: '>=0.4.0'}
2606 hasBin: true 2623 hasBin: true
2607 2624
2625 + add-stream@1.0.0:
2626 + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
2627 +
2608 address@1.2.2: 2628 address@1.2.2:
2609 resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} 2629 resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
2610 engines: {node: '>= 10.0.0'} 2630 engines: {node: '>= 10.0.0'}
...@@ -2666,6 +2686,10 @@ packages: ...@@ -2666,6 +2686,10 @@ packages:
2666 resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} 2686 resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
2667 engines: {node: '>=12'} 2687 engines: {node: '>=12'}
2668 2688
2689 + ansi-styles@3.2.1:
2690 + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
2691 + engines: {node: '>=4'}
2692 +
2669 ansi-styles@4.3.0: 2693 ansi-styles@4.3.0:
2670 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 2694 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
2671 engines: {node: '>=8'} 2695 engines: {node: '>=8'}
...@@ -2705,6 +2729,9 @@ packages: ...@@ -2705,6 +2729,9 @@ packages:
2705 array-flatten@1.1.1: 2729 array-flatten@1.1.1:
2706 resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 2730 resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
2707 2731
2732 + array-ify@1.0.0:
2733 + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
2734 +
2708 array-includes@3.1.9: 2735 array-includes@3.1.9:
2709 resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} 2736 resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
2710 engines: {node: '>= 0.4'} 2737 engines: {node: '>= 0.4'}
...@@ -2737,6 +2764,10 @@ packages: ...@@ -2737,6 +2764,10 @@ packages:
2737 resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} 2764 resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
2738 engines: {node: '>= 0.4'} 2765 engines: {node: '>= 0.4'}
2739 2766
2767 + arrify@1.0.1:
2768 + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
2769 + engines: {node: '>=0.10.0'}
2770 +
2740 assertion-error@1.1.0: 2771 assertion-error@1.1.0:
2741 resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} 2772 resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
2742 2773
...@@ -2957,6 +2988,10 @@ packages: ...@@ -2957,6 +2988,10 @@ packages:
2957 resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 2988 resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
2958 engines: {node: '>= 6'} 2989 engines: {node: '>= 6'}
2959 2990
2991 + camelcase-keys@6.2.2:
2992 + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
2993 + engines: {node: '>=8'}
2994 +
2960 camelcase@5.3.1: 2995 camelcase@5.3.1:
2961 resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} 2996 resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
2962 engines: {node: '>=6'} 2997 engines: {node: '>=6'}
...@@ -2981,6 +3016,10 @@ packages: ...@@ -2981,6 +3016,10 @@ packages:
2981 resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} 3016 resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
2982 engines: {node: '>=4'} 3017 engines: {node: '>=4'}
2983 3018
3019 + chalk@2.4.2:
3020 + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
3021 + engines: {node: '>=4'}
3022 +
2984 chalk@3.0.0: 3023 chalk@3.0.0:
2985 resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} 3024 resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
2986 engines: {node: '>=8'} 3025 engines: {node: '>=8'}
...@@ -3083,10 +3122,16 @@ packages: ...@@ -3083,10 +3122,16 @@ packages:
3083 resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} 3122 resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
3084 engines: {node: '>=0.8'} 3123 engines: {node: '>=0.8'}
3085 3124
3125 + color-convert@1.9.3:
3126 + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
3127 +
3086 color-convert@2.0.1: 3128 color-convert@2.0.1:
3087 resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 3129 resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
3088 engines: {node: '>=7.0.0'} 3130 engines: {node: '>=7.0.0'}
3089 3131
3132 + color-name@1.1.3:
3133 + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
3134 +
3090 color-name@1.1.4: 3135 color-name@1.1.4:
3091 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 3136 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
3092 3137
...@@ -3126,6 +3171,9 @@ packages: ...@@ -3126,6 +3171,9 @@ packages:
3126 commondir@1.0.1: 3171 commondir@1.0.1:
3127 resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} 3172 resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
3128 3173
3174 + compare-func@2.0.0:
3175 + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
3176 +
3129 compressible@2.0.18: 3177 compressible@2.0.18:
3130 resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} 3178 resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
3131 engines: {node: '>= 0.6'} 3179 engines: {node: '>= 0.6'}
...@@ -3137,6 +3185,10 @@ packages: ...@@ -3137,6 +3185,10 @@ packages:
3137 concat-map@0.0.1: 3185 concat-map@0.0.1:
3138 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 3186 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
3139 3187
3188 + concat-stream@2.0.0:
3189 + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
3190 + engines: {'0': node >= 6.0}
3191 +
3140 confbox@0.1.8: 3192 confbox@0.1.8:
3141 resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 3193 resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
3142 3194
...@@ -3168,6 +3220,76 @@ packages: ...@@ -3168,6 +3220,76 @@ packages:
3168 resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 3220 resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
3169 engines: {node: '>= 0.6'} 3221 engines: {node: '>= 0.6'}
3170 3222
3223 + conventional-changelog-angular@5.0.13:
3224 + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==}
3225 + engines: {node: '>=10'}
3226 +
3227 + conventional-changelog-atom@2.0.8:
3228 + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==}
3229 + engines: {node: '>=10'}
3230 +
3231 + conventional-changelog-codemirror@2.0.8:
3232 + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==}
3233 + engines: {node: '>=10'}
3234 +
3235 + conventional-changelog-config-spec@2.1.0:
3236 + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==}
3237 +
3238 + conventional-changelog-conventionalcommits@4.6.3:
3239 + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==}
3240 + engines: {node: '>=10'}
3241 +
3242 + conventional-changelog-core@4.2.4:
3243 + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==}
3244 + engines: {node: '>=10'}
3245 +
3246 + conventional-changelog-ember@2.0.9:
3247 + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==}
3248 + engines: {node: '>=10'}
3249 +
3250 + conventional-changelog-eslint@3.0.9:
3251 + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==}
3252 + engines: {node: '>=10'}
3253 +
3254 + conventional-changelog-express@2.0.6:
3255 + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==}
3256 + engines: {node: '>=10'}
3257 +
3258 + conventional-changelog-jquery@3.0.11:
3259 + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==}
3260 + engines: {node: '>=10'}
3261 +
3262 + conventional-changelog-jshint@2.0.9:
3263 + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==}
3264 + engines: {node: '>=10'}
3265 +
3266 + conventional-changelog-preset-loader@2.3.4:
3267 + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==}
3268 + engines: {node: '>=10'}
3269 +
3270 + conventional-changelog-writer@5.0.1:
3271 + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==}
3272 + engines: {node: '>=10'}
3273 + hasBin: true
3274 +
3275 + conventional-changelog@3.1.25:
3276 + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==}
3277 + engines: {node: '>=10'}
3278 +
3279 + conventional-commits-filter@2.0.7:
3280 + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==}
3281 + engines: {node: '>=10'}
3282 +
3283 + conventional-commits-parser@3.2.4:
3284 + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==}
3285 + engines: {node: '>=10'}
3286 + hasBin: true
3287 +
3288 + conventional-recommended-bump@6.1.0:
3289 + resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==}
3290 + engines: {node: '>=10'}
3291 + hasBin: true
3292 +
3171 convert-source-map@1.9.0: 3293 convert-source-map@1.9.0:
3172 resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} 3294 resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
3173 3295
...@@ -3355,6 +3477,10 @@ packages: ...@@ -3355,6 +3477,10 @@ packages:
3355 cuint@0.2.2: 3477 cuint@0.2.2:
3356 resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} 3478 resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==}
3357 3479
3480 + dargs@7.0.0:
3481 + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
3482 + engines: {node: '>=8'}
3483 +
3358 data-urls@5.0.0: 3484 data-urls@5.0.0:
3359 resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} 3485 resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
3360 engines: {node: '>=18'} 3486 engines: {node: '>=18'}
...@@ -3371,6 +3497,9 @@ packages: ...@@ -3371,6 +3497,9 @@ packages:
3371 resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} 3497 resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
3372 engines: {node: '>= 0.4'} 3498 engines: {node: '>= 0.4'}
3373 3499
3500 + dateformat@3.0.3:
3501 + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
3502 +
3374 dayjs@1.11.19: 3503 dayjs@1.11.19:
3375 resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} 3504 resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
3376 3505
...@@ -3399,6 +3528,10 @@ packages: ...@@ -3399,6 +3528,10 @@ packages:
3399 supports-color: 3528 supports-color:
3400 optional: true 3529 optional: true
3401 3530
3531 + decamelize-keys@1.1.1:
3532 + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
3533 + engines: {node: '>=0.10.0'}
3534 +
3402 decamelize@1.2.0: 3535 decamelize@1.2.0:
3403 resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} 3536 resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
3404 engines: {node: '>=0.10.0'} 3537 engines: {node: '>=0.10.0'}
...@@ -3497,10 +3630,18 @@ packages: ...@@ -3497,10 +3630,18 @@ packages:
3497 resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 3630 resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
3498 engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 3631 engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
3499 3632
3633 + detect-indent@6.1.0:
3634 + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
3635 + engines: {node: '>=8'}
3636 +
3500 detect-libc@2.1.2: 3637 detect-libc@2.1.2:
3501 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 3638 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
3502 engines: {node: '>=8'} 3639 engines: {node: '>=8'}
3503 3640
3641 + detect-newline@3.1.0:
3642 + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
3643 + engines: {node: '>=8'}
3644 +
3504 detect-node@2.1.0: 3645 detect-node@2.1.0:
3505 resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} 3646 resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
3506 3647
...@@ -3570,6 +3711,10 @@ packages: ...@@ -3570,6 +3711,10 @@ packages:
3570 dot-case@3.0.4: 3711 dot-case@3.0.4:
3571 resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} 3712 resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
3572 3713
3714 + dot-prop@5.3.0:
3715 + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
3716 + engines: {node: '>=8'}
3717 +
3573 dotenv-expand@11.0.7: 3718 dotenv-expand@11.0.7:
3574 resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} 3719 resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
3575 engines: {node: '>=12'} 3720 engines: {node: '>=12'}
...@@ -3582,6 +3727,10 @@ packages: ...@@ -3582,6 +3727,10 @@ packages:
3582 resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} 3727 resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
3583 engines: {node: '>=12'} 3728 engines: {node: '>=12'}
3584 3729
3730 + dotgitignore@2.1.0:
3731 + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==}
3732 + engines: {node: '>=6'}
3733 +
3585 download-git-repo@3.0.2: 3734 download-git-repo@3.0.2:
3586 resolution: {integrity: sha512-N8hWXD4hXqmEcNoR8TBYFntaOcYvEQ7Bz90mgm3bZRTuteGQqwT32VDMnTyD0KTEvb8BWrMc1tVmzuV9u/WrAg==} 3735 resolution: {integrity: sha512-N8hWXD4hXqmEcNoR8TBYFntaOcYvEQ7Bz90mgm3bZRTuteGQqwT32VDMnTyD0KTEvb8BWrMc1tVmzuV9u/WrAg==}
3587 3736
...@@ -4000,6 +4149,10 @@ packages: ...@@ -4000,6 +4149,10 @@ packages:
4000 resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} 4149 resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
4001 engines: {node: '>=6'} 4150 engines: {node: '>=6'}
4002 4151
4152 + find-up@2.1.0:
4153 + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==}
4154 + engines: {node: '>=4'}
4155 +
4003 find-up@3.0.0: 4156 find-up@3.0.0:
4004 resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} 4157 resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
4005 engines: {node: '>=6'} 4158 engines: {node: '>=6'}
...@@ -4116,6 +4269,11 @@ packages: ...@@ -4116,6 +4269,11 @@ packages:
4116 resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 4269 resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
4117 engines: {node: '>= 0.4'} 4270 engines: {node: '>= 0.4'}
4118 4271
4272 + get-pkg-repo@4.2.1:
4273 + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==}
4274 + engines: {node: '>=6.9.0'}
4275 + hasBin: true
4276 +
4119 get-proto@1.0.1: 4277 get-proto@1.0.1:
4120 resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 4278 resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
4121 engines: {node: '>= 0.4'} 4279 engines: {node: '>= 0.4'}
...@@ -4162,6 +4320,23 @@ packages: ...@@ -4162,6 +4320,23 @@ packages:
4162 git-clone@0.1.0: 4320 git-clone@0.1.0:
4163 resolution: {integrity: sha512-zs9rlfa7HyaJAKG9o+V7C6qfMzyc+tb1IIXdUFcOBcR1U7siKy/uPdauLlrH1mc0vOgUwIv4BF+QxPiiTYz3Rw==} 4321 resolution: {integrity: sha512-zs9rlfa7HyaJAKG9o+V7C6qfMzyc+tb1IIXdUFcOBcR1U7siKy/uPdauLlrH1mc0vOgUwIv4BF+QxPiiTYz3Rw==}
4164 4322
4323 + git-raw-commits@2.0.11:
4324 + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
4325 + engines: {node: '>=10'}
4326 + hasBin: true
4327 +
4328 + git-remote-origin-url@2.0.0:
4329 + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==}
4330 + engines: {node: '>=4'}
4331 +
4332 + git-semver-tags@4.1.1:
4333 + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==}
4334 + engines: {node: '>=10'}
4335 + hasBin: true
4336 +
4337 + gitconfiglocal@1.0.0:
4338 + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==}
4339 +
4165 glob-parent@5.1.2: 4340 glob-parent@5.1.2:
4166 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 4341 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
4167 engines: {node: '>= 6'} 4342 engines: {node: '>= 6'}
...@@ -4232,10 +4407,19 @@ packages: ...@@ -4232,10 +4407,19 @@ packages:
4232 handle-thing@2.0.1: 4407 handle-thing@2.0.1:
4233 resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} 4408 resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
4234 4409
4410 + handlebars@4.7.8:
4411 + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
4412 + engines: {node: '>=0.4.7'}
4413 + hasBin: true
4414 +
4235 happy-dom@14.12.3: 4415 happy-dom@14.12.3:
4236 resolution: {integrity: sha512-vsYlEs3E9gLwA1Hp+w3qzu+RUDFf4VTT8cyKqVICoZ2k7WM++Qyd2LwzyTi5bqMJFiIC/vNpTDYuxdreENRK/g==} 4416 resolution: {integrity: sha512-vsYlEs3E9gLwA1Hp+w3qzu+RUDFf4VTT8cyKqVICoZ2k7WM++Qyd2LwzyTi5bqMJFiIC/vNpTDYuxdreENRK/g==}
4237 engines: {node: '>=16.0.0'} 4417 engines: {node: '>=16.0.0'}
4238 4418
4419 + hard-rejection@2.1.0:
4420 + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
4421 + engines: {node: '>=6'}
4422 +
4239 harmony-reflect@1.6.2: 4423 harmony-reflect@1.6.2:
4240 resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==} 4424 resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==}
4241 4425
...@@ -4243,6 +4427,10 @@ packages: ...@@ -4243,6 +4427,10 @@ packages:
4243 resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} 4427 resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
4244 engines: {node: '>= 0.4'} 4428 engines: {node: '>= 0.4'}
4245 4429
4430 + has-flag@3.0.0:
4431 + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
4432 + engines: {node: '>=4'}
4433 +
4246 has-flag@4.0.0: 4434 has-flag@4.0.0:
4247 resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 4435 resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
4248 engines: {node: '>=8'} 4436 engines: {node: '>=8'}
...@@ -4294,6 +4482,13 @@ packages: ...@@ -4294,6 +4482,13 @@ packages:
4294 hookable@5.5.3: 4482 hookable@5.5.3:
4295 resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} 4483 resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
4296 4484
4485 + hosted-git-info@2.8.9:
4486 + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
4487 +
4488 + hosted-git-info@4.1.0:
4489 + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
4490 + engines: {node: '>=10'}
4491 +
4297 hpack.js@2.1.6: 4492 hpack.js@2.1.6:
4298 resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} 4493 resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
4299 4494
...@@ -4445,6 +4640,10 @@ packages: ...@@ -4445,6 +4640,10 @@ packages:
4445 resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 4640 resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
4446 engines: {node: '>=0.8.19'} 4641 engines: {node: '>=0.8.19'}
4447 4642
4643 + indent-string@4.0.0:
4644 + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
4645 + engines: {node: '>=8'}
4646 +
4448 inflight@1.0.6: 4647 inflight@1.0.6:
4449 resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 4648 resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
4450 deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 4649 deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
...@@ -4579,6 +4778,10 @@ packages: ...@@ -4579,6 +4778,10 @@ packages:
4579 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 4778 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
4580 engines: {node: '>=0.12.0'} 4779 engines: {node: '>=0.12.0'}
4581 4780
4781 + is-obj@2.0.0:
4782 + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
4783 + engines: {node: '>=8'}
4784 +
4582 is-object@1.0.2: 4785 is-object@1.0.2:
4583 resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} 4786 resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==}
4584 4787
...@@ -4637,6 +4840,10 @@ packages: ...@@ -4637,6 +4840,10 @@ packages:
4637 resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} 4840 resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
4638 engines: {node: '>= 0.4'} 4841 engines: {node: '>= 0.4'}
4639 4842
4843 + is-text-path@1.0.1:
4844 + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
4845 + engines: {node: '>=0.10.0'}
4846 +
4640 is-typed-array@1.1.15: 4847 is-typed-array@1.1.15:
4641 resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} 4848 resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
4642 engines: {node: '>= 0.4'} 4849 engines: {node: '>= 0.4'}
...@@ -4764,6 +4971,9 @@ packages: ...@@ -4764,6 +4971,9 @@ packages:
4764 json-buffer@3.0.1: 4971 json-buffer@3.0.1:
4765 resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 4972 resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
4766 4973
4974 + json-parse-better-errors@1.0.2:
4975 + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
4976 +
4767 json-parse-even-better-errors@2.3.1: 4977 json-parse-even-better-errors@2.3.1:
4768 resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 4978 resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
4769 4979
...@@ -4776,6 +4986,9 @@ packages: ...@@ -4776,6 +4986,9 @@ packages:
4776 json-stable-stringify-without-jsonify@1.0.1: 4986 json-stable-stringify-without-jsonify@1.0.1:
4777 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 4987 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
4778 4988
4989 + json-stringify-safe@5.0.1:
4990 + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
4991 +
4779 json5@1.0.2: 4992 json5@1.0.2:
4780 resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} 4993 resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
4781 hasBin: true 4994 hasBin: true
...@@ -4794,6 +5007,10 @@ packages: ...@@ -4794,6 +5007,10 @@ packages:
4794 jsonp-retry@1.0.3: 5007 jsonp-retry@1.0.3:
4795 resolution: {integrity: sha512-/jmE9+shtKP+oIt2AWO9Wx+C27NTGpLCEw4QHOqpoV2X6ta374HE9C+EEdgu8r3iLKgFMx7u5j0mCwxWN8UdlA==} 5008 resolution: {integrity: sha512-/jmE9+shtKP+oIt2AWO9Wx+C27NTGpLCEw4QHOqpoV2X6ta374HE9C+EEdgu8r3iLKgFMx7u5j0mCwxWN8UdlA==}
4796 5009
5010 + jsonparse@1.3.1:
5011 + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
5012 + engines: {'0': node >= 0.2.0}
5013 +
4797 jsx-ast-utils@3.3.5: 5014 jsx-ast-utils@3.3.5:
4798 resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} 5015 resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
4799 engines: {node: '>=4.0'} 5016 engines: {node: '>=4.0'}
...@@ -5013,6 +5230,10 @@ packages: ...@@ -5013,6 +5230,10 @@ packages:
5013 resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} 5230 resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
5014 engines: {node: '>=20.0.0'} 5231 engines: {node: '>=20.0.0'}
5015 5232
5233 + load-json-file@4.0.0:
5234 + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
5235 + engines: {node: '>=4'}
5236 +
5016 loader-runner@4.3.1: 5237 loader-runner@4.3.1:
5017 resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} 5238 resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
5018 engines: {node: '>=6.11.5'} 5239 engines: {node: '>=6.11.5'}
...@@ -5041,6 +5262,10 @@ packages: ...@@ -5041,6 +5262,10 @@ packages:
5041 resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} 5262 resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
5042 engines: {node: '>=14'} 5263 engines: {node: '>=14'}
5043 5264
5265 + locate-path@2.0.0:
5266 + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
5267 + engines: {node: '>=4'}
5268 +
5044 locate-path@3.0.0: 5269 locate-path@3.0.0:
5045 resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} 5270 resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
5046 engines: {node: '>=6'} 5271 engines: {node: '>=6'}
...@@ -5062,6 +5287,9 @@ packages: ...@@ -5062,6 +5287,9 @@ packages:
5062 lodash.debounce@4.0.8: 5287 lodash.debounce@4.0.8:
5063 resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} 5288 resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
5064 5289
5290 + lodash.ismatch@4.4.0:
5291 + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==}
5292 +
5065 lodash.memoize@4.1.2: 5293 lodash.memoize@4.1.2:
5066 resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} 5294 resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
5067 5295
...@@ -5123,6 +5351,10 @@ packages: ...@@ -5123,6 +5351,10 @@ packages:
5123 lru-cache@5.1.1: 5351 lru-cache@5.1.1:
5124 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 5352 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
5125 5353
5354 + lru-cache@6.0.0:
5355 + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
5356 + engines: {node: '>=10'}
5357 +
5126 magic-string@0.30.21: 5358 magic-string@0.30.21:
5127 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 5359 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
5128 5360
...@@ -5138,6 +5370,14 @@ packages: ...@@ -5138,6 +5370,14 @@ packages:
5138 resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} 5370 resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
5139 engines: {node: '>=8'} 5371 engines: {node: '>=8'}
5140 5372
5373 + map-obj@1.0.1:
5374 + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
5375 + engines: {node: '>=0.10.0'}
5376 +
5377 + map-obj@4.3.0:
5378 + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
5379 + engines: {node: '>=8'}
5380 +
5141 math-intrinsics@1.1.0: 5381 math-intrinsics@1.1.0:
5142 resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 5382 resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
5143 engines: {node: '>= 0.4'} 5383 engines: {node: '>= 0.4'}
...@@ -5162,6 +5402,10 @@ packages: ...@@ -5162,6 +5402,10 @@ packages:
5162 resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} 5402 resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
5163 engines: {node: '>= 4.0.0'} 5403 engines: {node: '>= 4.0.0'}
5164 5404
5405 + meow@8.1.2:
5406 + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
5407 + engines: {node: '>=10'}
5408 +
5165 merge-descriptors@1.0.3: 5409 merge-descriptors@1.0.3:
5166 resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} 5410 resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
5167 5411
...@@ -5218,6 +5462,10 @@ packages: ...@@ -5218,6 +5462,10 @@ packages:
5218 resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} 5462 resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
5219 engines: {node: '>=4'} 5463 engines: {node: '>=4'}
5220 5464
5465 + min-indent@1.0.1:
5466 + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
5467 + engines: {node: '>=4'}
5468 +
5221 mini-css-extract-plugin@2.10.0: 5469 mini-css-extract-plugin@2.10.0:
5222 resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} 5470 resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==}
5223 engines: {node: '>= 12.13.0'} 5471 engines: {node: '>= 12.13.0'}
...@@ -5249,6 +5497,10 @@ packages: ...@@ -5249,6 +5497,10 @@ packages:
5249 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 5497 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
5250 engines: {node: '>=16 || 14 >=14.17'} 5498 engines: {node: '>=16 || 14 >=14.17'}
5251 5499
5500 + minimist-options@4.1.0:
5501 + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
5502 + engines: {node: '>= 6'}
5503 +
5252 minimist@1.2.8: 5504 minimist@1.2.8:
5253 resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 5505 resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
5254 5506
...@@ -5281,6 +5533,10 @@ packages: ...@@ -5281,6 +5533,10 @@ packages:
5281 mobile-detect@1.4.5: 5533 mobile-detect@1.4.5:
5282 resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==} 5534 resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==}
5283 5535
5536 + modify-values@1.0.1:
5537 + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
5538 + engines: {node: '>=0.10.0'}
5539 +
5284 ms@2.0.0: 5540 ms@2.0.0:
5285 resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 5541 resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
5286 5542
...@@ -5352,6 +5608,13 @@ packages: ...@@ -5352,6 +5608,13 @@ packages:
5352 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 5608 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
5353 hasBin: true 5609 hasBin: true
5354 5610
5611 + normalize-package-data@2.5.0:
5612 + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
5613 +
5614 + normalize-package-data@3.0.3:
5615 + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
5616 + engines: {node: '>=10'}
5617 +
5355 normalize-path@3.0.0: 5618 normalize-path@3.0.0:
5356 resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 5619 resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
5357 engines: {node: '>=0.10.0'} 5620 engines: {node: '>=0.10.0'}
...@@ -5488,6 +5751,10 @@ packages: ...@@ -5488,6 +5751,10 @@ packages:
5488 resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==} 5751 resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==}
5489 engines: {node: '>=4'} 5752 engines: {node: '>=4'}
5490 5753
5754 + p-limit@1.3.0:
5755 + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==}
5756 + engines: {node: '>=4'}
5757 +
5491 p-limit@2.3.0: 5758 p-limit@2.3.0:
5492 resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} 5759 resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
5493 engines: {node: '>=6'} 5760 engines: {node: '>=6'}
...@@ -5500,6 +5767,10 @@ packages: ...@@ -5500,6 +5767,10 @@ packages:
5500 resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} 5767 resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
5501 engines: {node: '>=18'} 5768 engines: {node: '>=18'}
5502 5769
5770 + p-locate@2.0.0:
5771 + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==}
5772 + engines: {node: '>=4'}
5773 +
5503 p-locate@3.0.0: 5774 p-locate@3.0.0:
5504 resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} 5775 resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
5505 engines: {node: '>=6'} 5776 engines: {node: '>=6'}
...@@ -5520,6 +5791,10 @@ packages: ...@@ -5520,6 +5791,10 @@ packages:
5520 resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} 5791 resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==}
5521 engines: {node: '>=4'} 5792 engines: {node: '>=4'}
5522 5793
5794 + p-try@1.0.0:
5795 + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
5796 + engines: {node: '>=4'}
5797 +
5523 p-try@2.2.0: 5798 p-try@2.2.0:
5524 resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} 5799 resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
5525 engines: {node: '>=6'} 5800 engines: {node: '>=6'}
...@@ -5541,6 +5816,10 @@ packages: ...@@ -5541,6 +5816,10 @@ packages:
5541 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 5816 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
5542 engines: {node: '>=6'} 5817 engines: {node: '>=6'}
5543 5818
5819 + parse-json@4.0.0:
5820 + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
5821 + engines: {node: '>=4'}
5822 +
5544 parse-json@5.2.0: 5823 parse-json@5.2.0:
5545 resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} 5824 resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
5546 engines: {node: '>=8'} 5825 engines: {node: '>=8'}
...@@ -5604,6 +5883,10 @@ packages: ...@@ -5604,6 +5883,10 @@ packages:
5604 path-to-regexp@6.3.0: 5883 path-to-regexp@6.3.0:
5605 resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 5884 resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
5606 5885
5886 + path-type@3.0.0:
5887 + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
5888 + engines: {node: '>=4'}
5889 +
5607 path-type@4.0.0: 5890 path-type@4.0.0:
5608 resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 5891 resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
5609 engines: {node: '>=8'} 5892 engines: {node: '>=8'}
...@@ -6233,6 +6516,14 @@ packages: ...@@ -6233,6 +6516,14 @@ packages:
6233 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 6516 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
6234 engines: {node: '>=6'} 6517 engines: {node: '>=6'}
6235 6518
6519 + q@1.5.1:
6520 + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
6521 + engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
6522 + deprecated: |-
6523 + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
6524 +
6525 + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
6526 +
6236 qrcode@1.5.4: 6527 qrcode@1.5.4:
6237 resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} 6528 resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
6238 engines: {node: '>=10.13.0'} 6529 engines: {node: '>=10.13.0'}
...@@ -6259,6 +6550,10 @@ packages: ...@@ -6259,6 +6550,10 @@ packages:
6259 queue-microtask@1.2.3: 6550 queue-microtask@1.2.3:
6260 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 6551 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
6261 6552
6553 + quick-lru@4.0.1:
6554 + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
6555 + engines: {node: '>=8'}
6556 +
6262 randombytes@2.1.0: 6557 randombytes@2.1.0:
6263 resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 6558 resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
6264 6559
...@@ -6293,6 +6588,22 @@ packages: ...@@ -6293,6 +6588,22 @@ packages:
6293 read-cache@1.0.0: 6588 read-cache@1.0.0:
6294 resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 6589 resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
6295 6590
6591 + read-pkg-up@3.0.0:
6592 + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==}
6593 + engines: {node: '>=4'}
6594 +
6595 + read-pkg-up@7.0.1:
6596 + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
6597 + engines: {node: '>=8'}
6598 +
6599 + read-pkg@3.0.0:
6600 + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
6601 + engines: {node: '>=4'}
6602 +
6603 + read-pkg@5.2.0:
6604 + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
6605 + engines: {node: '>=8'}
6606 +
6296 readable-stream@2.3.8: 6607 readable-stream@2.3.8:
6297 resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} 6608 resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
6298 6609
...@@ -6312,6 +6623,10 @@ packages: ...@@ -6312,6 +6623,10 @@ packages:
6312 resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} 6623 resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
6313 engines: {node: '>= 20.19.0'} 6624 engines: {node: '>= 20.19.0'}
6314 6625
6626 + redent@3.0.0:
6627 + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
6628 + engines: {node: '>=8'}
6629 +
6315 reflect.getprototypeof@1.0.10: 6630 reflect.getprototypeof@1.0.10:
6316 resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} 6631 resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
6317 engines: {node: '>= 0.4'} 6632 engines: {node: '>= 0.4'}
...@@ -6698,6 +7013,18 @@ packages: ...@@ -6698,6 +7013,18 @@ packages:
6698 resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} 7013 resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
6699 engines: {node: '>= 12'} 7014 engines: {node: '>= 12'}
6700 7015
7016 + spdx-correct@3.2.0:
7017 + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
7018 +
7019 + spdx-exceptions@2.5.0:
7020 + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
7021 +
7022 + spdx-expression-parse@3.0.1:
7023 + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
7024 +
7025 + spdx-license-ids@3.0.22:
7026 + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
7027 +
6701 spdy-transport@3.0.0: 7028 spdy-transport@3.0.0:
6702 resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} 7029 resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==}
6703 7030
...@@ -6713,9 +7040,20 @@ packages: ...@@ -6713,9 +7040,20 @@ packages:
6713 resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} 7040 resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
6714 engines: {node: '>=12'} 7041 engines: {node: '>=12'}
6715 7042
7043 + split2@3.2.2:
7044 + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
7045 +
7046 + split@1.0.1:
7047 + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
7048 +
6716 stackback@0.0.2: 7049 stackback@0.0.2:
6717 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 7050 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
6718 7051
7052 + standard-version@9.5.0:
7053 + resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==}
7054 + engines: {node: '>=10'}
7055 + hasBin: true
7056 +
6719 statuses@1.5.0: 7057 statuses@1.5.0:
6720 resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} 7058 resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
6721 engines: {node: '>= 0.6'} 7059 engines: {node: '>= 0.6'}
...@@ -6783,6 +7121,10 @@ packages: ...@@ -6783,6 +7121,10 @@ packages:
6783 string_decoder@1.3.0: 7121 string_decoder@1.3.0:
6784 resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 7122 resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
6785 7123
7124 + stringify-package@1.0.1:
7125 + resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==}
7126 + deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json
7127 +
6786 strip-ansi@6.0.1: 7128 strip-ansi@6.0.1:
6787 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 7129 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
6788 engines: {node: '>=8'} 7130 engines: {node: '>=8'}
...@@ -6806,6 +7148,10 @@ packages: ...@@ -6806,6 +7148,10 @@ packages:
6806 resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 7148 resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
6807 engines: {node: '>=12'} 7149 engines: {node: '>=12'}
6808 7150
7151 + strip-indent@3.0.0:
7152 + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
7153 + engines: {node: '>=8'}
7154 +
6809 strip-json-comments@2.0.1: 7155 strip-json-comments@2.0.1:
6810 resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 7156 resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
6811 engines: {node: '>=0.10.0'} 7157 engines: {node: '>=0.10.0'}
...@@ -6866,6 +7212,10 @@ packages: ...@@ -6866,6 +7212,10 @@ packages:
6866 resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} 7212 resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
6867 engines: {node: '>=16'} 7213 engines: {node: '>=16'}
6868 7214
7215 + supports-color@5.5.0:
7216 + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
7217 + engines: {node: '>=4'}
7218 +
6869 supports-color@7.2.0: 7219 supports-color@7.2.0:
6870 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 7220 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
6871 engines: {node: '>=8'} 7221 engines: {node: '>=8'}
...@@ -6947,6 +7297,10 @@ packages: ...@@ -6947,6 +7297,10 @@ packages:
6947 engines: {node: '>=10'} 7297 engines: {node: '>=10'}
6948 hasBin: true 7298 hasBin: true
6949 7299
7300 + text-extensions@1.9.0:
7301 + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==}
7302 + engines: {node: '>=0.10'}
7303 +
6950 text-table@0.2.0: 7304 text-table@0.2.0:
6951 resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 7305 resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
6952 7306
...@@ -6957,6 +7311,12 @@ packages: ...@@ -6957,6 +7311,12 @@ packages:
6957 thenify@3.3.1: 7311 thenify@3.3.1:
6958 resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 7312 resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
6959 7313
7314 + through2@2.0.5:
7315 + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
7316 +
7317 + through2@4.0.2:
7318 + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
7319 +
6960 through@2.3.8: 7320 through@2.3.8:
6961 resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} 7321 resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
6962 7322
...@@ -7016,6 +7376,10 @@ packages: ...@@ -7016,6 +7376,10 @@ packages:
7016 resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} 7376 resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
7017 engines: {node: '>=18'} 7377 engines: {node: '>=18'}
7018 7378
7379 + trim-newlines@3.0.1:
7380 + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
7381 + engines: {node: '>=8'}
7382 +
7019 trim-repeated@1.0.0: 7383 trim-repeated@1.0.0:
7020 resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} 7384 resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
7021 engines: {node: '>=0.10.0'} 7385 engines: {node: '>=0.10.0'}
...@@ -7052,6 +7416,10 @@ packages: ...@@ -7052,6 +7416,10 @@ packages:
7052 resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} 7416 resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
7053 engines: {node: '>=4'} 7417 engines: {node: '>=4'}
7054 7418
7419 + type-fest@0.18.1:
7420 + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
7421 + engines: {node: '>=10'}
7422 +
7055 type-fest@0.20.2: 7423 type-fest@0.20.2:
7056 resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} 7424 resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
7057 engines: {node: '>=10'} 7425 engines: {node: '>=10'}
...@@ -7060,6 +7428,14 @@ packages: ...@@ -7060,6 +7428,14 @@ packages:
7060 resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} 7428 resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
7061 engines: {node: '>=10'} 7429 engines: {node: '>=10'}
7062 7430
7431 + type-fest@0.6.0:
7432 + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
7433 + engines: {node: '>=8'}
7434 +
7435 + type-fest@0.8.1:
7436 + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
7437 + engines: {node: '>=8'}
7438 +
7063 type-fest@2.19.0: 7439 type-fest@2.19.0:
7064 resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} 7440 resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
7065 engines: {node: '>=12.20'} 7441 engines: {node: '>=12.20'}
...@@ -7084,6 +7460,9 @@ packages: ...@@ -7084,6 +7460,9 @@ packages:
7084 resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} 7460 resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
7085 engines: {node: '>= 0.4'} 7461 engines: {node: '>= 0.4'}
7086 7462
7463 + typedarray@0.0.6:
7464 + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
7465 +
7087 typescript@5.9.3: 7466 typescript@5.9.3:
7088 resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 7467 resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
7089 engines: {node: '>=14.17'} 7468 engines: {node: '>=14.17'}
...@@ -7212,6 +7591,9 @@ packages: ...@@ -7212,6 +7591,9 @@ packages:
7212 validate-html-nesting@1.2.4: 7591 validate-html-nesting@1.2.4:
7213 resolution: {integrity: sha512-doQi7e8EJ2OWneSG1aZpJluS6A49aZM0+EICXWKm1i6WvqTLmq0tpUcImc4KTWG50mORO0C4YDBtOCSYvElftw==} 7592 resolution: {integrity: sha512-doQi7e8EJ2OWneSG1aZpJluS6A49aZM0+EICXWKm1i6WvqTLmq0tpUcImc4KTWG50mORO0C4YDBtOCSYvElftw==}
7214 7593
7594 + validate-npm-package-license@3.0.4:
7595 + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
7596 +
7215 validate-npm-package-name@5.0.1: 7597 validate-npm-package-name@5.0.1:
7216 resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} 7598 resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
7217 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 7599 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
...@@ -7466,6 +7848,9 @@ packages: ...@@ -7466,6 +7848,9 @@ packages:
7466 resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 7848 resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
7467 engines: {node: '>=0.10.0'} 7849 engines: {node: '>=0.10.0'}
7468 7850
7851 + wordwrap@1.0.0:
7852 + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
7853 +
7469 wrap-ansi@6.2.0: 7854 wrap-ansi@6.2.0:
7470 resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 7855 resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
7471 engines: {node: '>=8'} 7856 engines: {node: '>=8'}
...@@ -7524,6 +7909,9 @@ packages: ...@@ -7524,6 +7909,9 @@ packages:
7524 yallist@3.1.1: 7909 yallist@3.1.1:
7525 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 7910 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
7526 7911
7912 + yallist@4.0.0:
7913 + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
7914 +
7527 yaml@2.8.2: 7915 yaml@2.8.2:
7528 resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} 7916 resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
7529 engines: {node: '>= 14.6'} 7917 engines: {node: '>= 14.6'}
...@@ -8854,6 +9242,8 @@ snapshots: ...@@ -8854,6 +9242,8 @@ snapshots:
8854 9242
8855 '@humanwhocodes/object-schema@2.0.3': {} 9243 '@humanwhocodes/object-schema@2.0.3': {}
8856 9244
9245 + '@hutson/parse-repository-url@3.0.2': {}
9246 +
8857 '@inquirer/external-editor@1.0.3(@types/node@25.2.2)': 9247 '@inquirer/external-editor@1.0.3(@types/node@25.2.2)':
8858 dependencies: 9248 dependencies:
8859 chardet: 2.1.1 9249 chardet: 2.1.1
...@@ -9760,6 +10150,8 @@ snapshots: ...@@ -9760,6 +10150,8 @@ snapshots:
9760 dependencies: 10150 dependencies:
9761 minimatch: 10.1.2 10151 minimatch: 10.1.2
9762 10152
10153 + '@types/minimist@1.2.5': {}
10154 +
9763 '@types/ms@2.1.0': {} 10155 '@types/ms@2.1.0': {}
9764 10156
9765 '@types/node-forge@1.3.14': 10157 '@types/node-forge@1.3.14':
...@@ -9774,6 +10166,8 @@ snapshots: ...@@ -9774,6 +10166,8 @@ snapshots:
9774 dependencies: 10166 dependencies:
9775 undici-types: 7.16.0 10167 undici-types: 7.16.0
9776 10168
10169 + '@types/normalize-package-data@2.4.4': {}
10170 +
9777 '@types/postcss-url@10.0.4': 10171 '@types/postcss-url@10.0.4':
9778 dependencies: 10172 dependencies:
9779 '@types/node': 25.2.2 10173 '@types/node': 25.2.2
...@@ -10172,6 +10566,11 @@ snapshots: ...@@ -10172,6 +10566,11 @@ snapshots:
10172 10566
10173 '@xtuc/long@4.2.2': {} 10567 '@xtuc/long@4.2.2': {}
10174 10568
10569 + JSONStream@1.3.5:
10570 + dependencies:
10571 + jsonparse: 1.3.1
10572 + through: 2.3.8
10573 +
10175 abbrev@2.0.0: {} 10574 abbrev@2.0.0: {}
10176 10575
10177 abortcontroller-polyfill@1.7.8: {} 10576 abortcontroller-polyfill@1.7.8: {}
...@@ -10195,6 +10594,8 @@ snapshots: ...@@ -10195,6 +10594,8 @@ snapshots:
10195 10594
10196 acorn@8.15.0: {} 10595 acorn@8.15.0: {}
10197 10596
10597 + add-stream@1.0.0: {}
10598 +
10198 address@1.2.2: {} 10599 address@1.2.2: {}
10199 10600
10200 adjust-sourcemap-loader@4.0.0: 10601 adjust-sourcemap-loader@4.0.0:
...@@ -10247,6 +10648,10 @@ snapshots: ...@@ -10247,6 +10648,10 @@ snapshots:
10247 10648
10248 ansi-regex@6.2.2: {} 10649 ansi-regex@6.2.2: {}
10249 10650
10651 + ansi-styles@3.2.1:
10652 + dependencies:
10653 + color-convert: 1.9.3
10654 +
10250 ansi-styles@4.3.0: 10655 ansi-styles@4.3.0:
10251 dependencies: 10656 dependencies:
10252 color-convert: 2.0.1 10657 color-convert: 2.0.1
...@@ -10279,6 +10684,8 @@ snapshots: ...@@ -10279,6 +10684,8 @@ snapshots:
10279 10684
10280 array-flatten@1.1.1: {} 10685 array-flatten@1.1.1: {}
10281 10686
10687 + array-ify@1.0.0: {}
10688 +
10282 array-includes@3.1.9: 10689 array-includes@3.1.9:
10283 dependencies: 10690 dependencies:
10284 call-bind: 1.0.8 10691 call-bind: 1.0.8
...@@ -10343,6 +10750,8 @@ snapshots: ...@@ -10343,6 +10750,8 @@ snapshots:
10343 get-intrinsic: 1.3.0 10750 get-intrinsic: 1.3.0
10344 is-array-buffer: 3.0.5 10751 is-array-buffer: 3.0.5
10345 10752
10753 + arrify@1.0.1: {}
10754 +
10346 assertion-error@1.1.0: {} 10755 assertion-error@1.1.0: {}
10347 10756
10348 async-function@1.0.0: {} 10757 async-function@1.0.0: {}
...@@ -10629,6 +11038,12 @@ snapshots: ...@@ -10629,6 +11038,12 @@ snapshots:
10629 11038
10630 camelcase-css@2.0.1: {} 11039 camelcase-css@2.0.1: {}
10631 11040
11041 + camelcase-keys@6.2.2:
11042 + dependencies:
11043 + camelcase: 5.3.1
11044 + map-obj: 4.3.0
11045 + quick-lru: 4.0.1
11046 +
10632 camelcase@5.3.1: {} 11047 camelcase@5.3.1: {}
10633 11048
10634 caniuse-api@3.0.0: 11049 caniuse-api@3.0.0:
...@@ -10665,6 +11080,12 @@ snapshots: ...@@ -10665,6 +11080,12 @@ snapshots:
10665 pathval: 1.1.1 11080 pathval: 1.1.1
10666 type-detect: 4.1.0 11081 type-detect: 4.1.0
10667 11082
11083 + chalk@2.4.2:
11084 + dependencies:
11085 + ansi-styles: 3.2.1
11086 + escape-string-regexp: 1.0.5
11087 + supports-color: 5.5.0
11088 +
10668 chalk@3.0.0: 11089 chalk@3.0.0:
10669 dependencies: 11090 dependencies:
10670 ansi-styles: 4.3.0 11091 ansi-styles: 4.3.0
...@@ -10792,10 +11213,16 @@ snapshots: ...@@ -10792,10 +11213,16 @@ snapshots:
10792 11213
10793 clone@1.0.4: {} 11214 clone@1.0.4: {}
10794 11215
11216 + color-convert@1.9.3:
11217 + dependencies:
11218 + color-name: 1.1.3
11219 +
10795 color-convert@2.0.1: 11220 color-convert@2.0.1:
10796 dependencies: 11221 dependencies:
10797 color-name: 1.1.4 11222 color-name: 1.1.4
10798 11223
11224 + color-name@1.1.3: {}
11225 +
10799 color-name@1.1.4: {} 11226 color-name@1.1.4: {}
10800 11227
10801 colord@2.9.3: {} 11228 colord@2.9.3: {}
...@@ -10820,6 +11247,11 @@ snapshots: ...@@ -10820,6 +11247,11 @@ snapshots:
10820 11247
10821 commondir@1.0.1: {} 11248 commondir@1.0.1: {}
10822 11249
11250 + compare-func@2.0.0:
11251 + dependencies:
11252 + array-ify: 1.0.0
11253 + dot-prop: 5.3.0
11254 +
10823 compressible@2.0.18: 11255 compressible@2.0.18:
10824 dependencies: 11256 dependencies:
10825 mime-db: 1.54.0 11257 mime-db: 1.54.0
...@@ -10838,6 +11270,13 @@ snapshots: ...@@ -10838,6 +11270,13 @@ snapshots:
10838 11270
10839 concat-map@0.0.1: {} 11271 concat-map@0.0.1: {}
10840 11272
11273 + concat-stream@2.0.0:
11274 + dependencies:
11275 + buffer-from: 1.1.2
11276 + inherits: 2.0.4
11277 + readable-stream: 3.6.2
11278 + typedarray: 0.0.6
11279 +
10841 confbox@0.1.8: {} 11280 confbox@0.1.8: {}
10842 11281
10843 confbox@0.2.2: {} 11282 confbox@0.2.2: {}
...@@ -10865,6 +11304,118 @@ snapshots: ...@@ -10865,6 +11304,118 @@ snapshots:
10865 11304
10866 content-type@1.0.5: {} 11305 content-type@1.0.5: {}
10867 11306
11307 + conventional-changelog-angular@5.0.13:
11308 + dependencies:
11309 + compare-func: 2.0.0
11310 + q: 1.5.1
11311 +
11312 + conventional-changelog-atom@2.0.8:
11313 + dependencies:
11314 + q: 1.5.1
11315 +
11316 + conventional-changelog-codemirror@2.0.8:
11317 + dependencies:
11318 + q: 1.5.1
11319 +
11320 + conventional-changelog-config-spec@2.1.0: {}
11321 +
11322 + conventional-changelog-conventionalcommits@4.6.3:
11323 + dependencies:
11324 + compare-func: 2.0.0
11325 + lodash: 4.17.23
11326 + q: 1.5.1
11327 +
11328 + conventional-changelog-core@4.2.4:
11329 + dependencies:
11330 + add-stream: 1.0.0
11331 + conventional-changelog-writer: 5.0.1
11332 + conventional-commits-parser: 3.2.4
11333 + dateformat: 3.0.3
11334 + get-pkg-repo: 4.2.1
11335 + git-raw-commits: 2.0.11
11336 + git-remote-origin-url: 2.0.0
11337 + git-semver-tags: 4.1.1
11338 + lodash: 4.17.23
11339 + normalize-package-data: 3.0.3
11340 + q: 1.5.1
11341 + read-pkg: 3.0.0
11342 + read-pkg-up: 3.0.0
11343 + through2: 4.0.2
11344 +
11345 + conventional-changelog-ember@2.0.9:
11346 + dependencies:
11347 + q: 1.5.1
11348 +
11349 + conventional-changelog-eslint@3.0.9:
11350 + dependencies:
11351 + q: 1.5.1
11352 +
11353 + conventional-changelog-express@2.0.6:
11354 + dependencies:
11355 + q: 1.5.1
11356 +
11357 + conventional-changelog-jquery@3.0.11:
11358 + dependencies:
11359 + q: 1.5.1
11360 +
11361 + conventional-changelog-jshint@2.0.9:
11362 + dependencies:
11363 + compare-func: 2.0.0
11364 + q: 1.5.1
11365 +
11366 + conventional-changelog-preset-loader@2.3.4: {}
11367 +
11368 + conventional-changelog-writer@5.0.1:
11369 + dependencies:
11370 + conventional-commits-filter: 2.0.7
11371 + dateformat: 3.0.3
11372 + handlebars: 4.7.8
11373 + json-stringify-safe: 5.0.1
11374 + lodash: 4.17.23
11375 + meow: 8.1.2
11376 + semver: 6.3.1
11377 + split: 1.0.1
11378 + through2: 4.0.2
11379 +
11380 + conventional-changelog@3.1.25:
11381 + dependencies:
11382 + conventional-changelog-angular: 5.0.13
11383 + conventional-changelog-atom: 2.0.8
11384 + conventional-changelog-codemirror: 2.0.8
11385 + conventional-changelog-conventionalcommits: 4.6.3
11386 + conventional-changelog-core: 4.2.4
11387 + conventional-changelog-ember: 2.0.9
11388 + conventional-changelog-eslint: 3.0.9
11389 + conventional-changelog-express: 2.0.6
11390 + conventional-changelog-jquery: 3.0.11
11391 + conventional-changelog-jshint: 2.0.9
11392 + conventional-changelog-preset-loader: 2.3.4
11393 +
11394 + conventional-commits-filter@2.0.7:
11395 + dependencies:
11396 + lodash.ismatch: 4.4.0
11397 + modify-values: 1.0.1
11398 +
11399 + conventional-commits-parser@3.2.4:
11400 + dependencies:
11401 + JSONStream: 1.3.5
11402 + is-text-path: 1.0.1
11403 + lodash: 4.17.23
11404 + meow: 8.1.2
11405 + split2: 3.2.2
11406 + through2: 4.0.2
11407 +
11408 + conventional-recommended-bump@6.1.0:
11409 + dependencies:
11410 + concat-stream: 2.0.0
11411 + conventional-changelog-preset-loader: 2.3.4
11412 + conventional-commits-filter: 2.0.7
11413 + conventional-commits-parser: 3.2.4
11414 + git-raw-commits: 2.0.11
11415 + git-semver-tags: 4.1.1
11416 + meow: 8.1.2
11417 + q: 1.5.1
11418 +
10868 convert-source-map@1.9.0: {} 11419 convert-source-map@1.9.0: {}
10869 11420
10870 convert-source-map@2.0.0: {} 11421 convert-source-map@2.0.0: {}
...@@ -11078,6 +11629,8 @@ snapshots: ...@@ -11078,6 +11629,8 @@ snapshots:
11078 11629
11079 cuint@0.2.2: {} 11630 cuint@0.2.2: {}
11080 11631
11632 + dargs@7.0.0: {}
11633 +
11081 data-urls@5.0.0: 11634 data-urls@5.0.0:
11082 dependencies: 11635 dependencies:
11083 whatwg-mimetype: 4.0.0 11636 whatwg-mimetype: 4.0.0
...@@ -11101,6 +11654,8 @@ snapshots: ...@@ -11101,6 +11654,8 @@ snapshots:
11101 es-errors: 1.3.0 11654 es-errors: 1.3.0
11102 is-data-view: 1.0.2 11655 is-data-view: 1.0.2
11103 11656
11657 + dateformat@3.0.3: {}
11658 +
11104 dayjs@1.11.19: {} 11659 dayjs@1.11.19: {}
11105 11660
11106 debug@2.6.9: 11661 debug@2.6.9:
...@@ -11115,6 +11670,11 @@ snapshots: ...@@ -11115,6 +11670,11 @@ snapshots:
11115 dependencies: 11670 dependencies:
11116 ms: 2.1.3 11671 ms: 2.1.3
11117 11672
11673 + decamelize-keys@1.1.1:
11674 + dependencies:
11675 + decamelize: 1.2.0
11676 + map-obj: 1.0.1
11677 +
11118 decamelize@1.2.0: {} 11678 decamelize@1.2.0: {}
11119 11679
11120 decimal.js@10.6.0: {} 11680 decimal.js@10.6.0: {}
...@@ -11211,8 +11771,12 @@ snapshots: ...@@ -11211,8 +11771,12 @@ snapshots:
11211 11771
11212 destroy@1.2.0: {} 11772 destroy@1.2.0: {}
11213 11773
11774 + detect-indent@6.1.0: {}
11775 +
11214 detect-libc@2.1.2: {} 11776 detect-libc@2.1.2: {}
11215 11777
11778 + detect-newline@3.1.0: {}
11779 +
11216 detect-node@2.1.0: {} 11780 detect-node@2.1.0: {}
11217 11781
11218 detect-port@1.6.1: 11782 detect-port@1.6.1:
...@@ -11293,6 +11857,10 @@ snapshots: ...@@ -11293,6 +11857,10 @@ snapshots:
11293 no-case: 3.0.4 11857 no-case: 3.0.4
11294 tslib: 2.8.1 11858 tslib: 2.8.1
11295 11859
11860 + dot-prop@5.3.0:
11861 + dependencies:
11862 + is-obj: 2.0.0
11863 +
11296 dotenv-expand@11.0.7: 11864 dotenv-expand@11.0.7:
11297 dependencies: 11865 dependencies:
11298 dotenv: 16.6.1 11866 dotenv: 16.6.1
...@@ -11301,6 +11869,11 @@ snapshots: ...@@ -11301,6 +11869,11 @@ snapshots:
11301 11869
11302 dotenv@17.2.3: {} 11870 dotenv@17.2.3: {}
11303 11871
11872 + dotgitignore@2.1.0:
11873 + dependencies:
11874 + find-up: 3.0.0
11875 + minimatch: 3.1.2
11876 +
11304 download-git-repo@3.0.2: 11877 download-git-repo@3.0.2:
11305 dependencies: 11878 dependencies:
11306 download: 7.1.0 11879 download: 7.1.0
...@@ -11964,6 +12537,10 @@ snapshots: ...@@ -11964,6 +12537,10 @@ snapshots:
11964 make-dir: 2.1.0 12537 make-dir: 2.1.0
11965 pkg-dir: 3.0.0 12538 pkg-dir: 3.0.0
11966 12539
12540 + find-up@2.1.0:
12541 + dependencies:
12542 + locate-path: 2.0.0
12543 +
11967 find-up@3.0.0: 12544 find-up@3.0.0:
11968 dependencies: 12545 dependencies:
11969 locate-path: 3.0.0 12546 locate-path: 3.0.0
...@@ -12079,6 +12656,13 @@ snapshots: ...@@ -12079,6 +12656,13 @@ snapshots:
12079 hasown: 2.0.2 12656 hasown: 2.0.2
12080 math-intrinsics: 1.1.0 12657 math-intrinsics: 1.1.0
12081 12658
12659 + get-pkg-repo@4.2.1:
12660 + dependencies:
12661 + '@hutson/parse-repository-url': 3.0.2
12662 + hosted-git-info: 4.1.0
12663 + through2: 2.0.5
12664 + yargs: 16.2.0
12665 +
12082 get-proto@1.0.1: 12666 get-proto@1.0.1:
12083 dependencies: 12667 dependencies:
12084 dunder-proto: 1.0.1 12668 dunder-proto: 1.0.1
...@@ -12128,6 +12712,28 @@ snapshots: ...@@ -12128,6 +12712,28 @@ snapshots:
12128 12712
12129 git-clone@0.1.0: {} 12713 git-clone@0.1.0: {}
12130 12714
12715 + git-raw-commits@2.0.11:
12716 + dependencies:
12717 + dargs: 7.0.0
12718 + lodash: 4.17.23
12719 + meow: 8.1.2
12720 + split2: 3.2.2
12721 + through2: 4.0.2
12722 +
12723 + git-remote-origin-url@2.0.0:
12724 + dependencies:
12725 + gitconfiglocal: 1.0.0
12726 + pify: 2.3.0
12727 +
12728 + git-semver-tags@4.1.1:
12729 + dependencies:
12730 + meow: 8.1.2
12731 + semver: 6.3.1
12732 +
12733 + gitconfiglocal@1.0.0:
12734 + dependencies:
12735 + ini: 1.3.8
12736 +
12131 glob-parent@5.1.2: 12737 glob-parent@5.1.2:
12132 dependencies: 12738 dependencies:
12133 is-glob: 4.0.3 12739 is-glob: 4.0.3
...@@ -12243,16 +12849,29 @@ snapshots: ...@@ -12243,16 +12849,29 @@ snapshots:
12243 12849
12244 handle-thing@2.0.1: {} 12850 handle-thing@2.0.1: {}
12245 12851
12852 + handlebars@4.7.8:
12853 + dependencies:
12854 + minimist: 1.2.8
12855 + neo-async: 2.6.2
12856 + source-map: 0.6.1
12857 + wordwrap: 1.0.0
12858 + optionalDependencies:
12859 + uglify-js: 3.19.3
12860 +
12246 happy-dom@14.12.3: 12861 happy-dom@14.12.3:
12247 dependencies: 12862 dependencies:
12248 entities: 4.5.0 12863 entities: 4.5.0
12249 webidl-conversions: 7.0.0 12864 webidl-conversions: 7.0.0
12250 whatwg-mimetype: 3.0.0 12865 whatwg-mimetype: 3.0.0
12251 12866
12867 + hard-rejection@2.1.0: {}
12868 +
12252 harmony-reflect@1.6.2: {} 12869 harmony-reflect@1.6.2: {}
12253 12870
12254 has-bigints@1.1.0: {} 12871 has-bigints@1.1.0: {}
12255 12872
12873 + has-flag@3.0.0: {}
12874 +
12256 has-flag@4.0.0: {} 12875 has-flag@4.0.0: {}
12257 12876
12258 has-property-descriptors@1.0.2: 12877 has-property-descriptors@1.0.2:
...@@ -12298,6 +12917,12 @@ snapshots: ...@@ -12298,6 +12917,12 @@ snapshots:
12298 12917
12299 hookable@5.5.3: {} 12918 hookable@5.5.3: {}
12300 12919
12920 + hosted-git-info@2.8.9: {}
12921 +
12922 + hosted-git-info@4.1.0:
12923 + dependencies:
12924 + lru-cache: 6.0.0
12925 +
12301 hpack.js@2.1.6: 12926 hpack.js@2.1.6:
12302 dependencies: 12927 dependencies:
12303 inherits: 2.0.4 12928 inherits: 2.0.4
...@@ -12469,6 +13094,8 @@ snapshots: ...@@ -12469,6 +13094,8 @@ snapshots:
12469 13094
12470 imurmurhash@0.1.4: {} 13095 imurmurhash@0.1.4: {}
12471 13096
13097 + indent-string@4.0.0: {}
13098 +
12472 inflight@1.0.6: 13099 inflight@1.0.6:
12473 dependencies: 13100 dependencies:
12474 once: 1.4.0 13101 once: 1.4.0
...@@ -12608,6 +13235,8 @@ snapshots: ...@@ -12608,6 +13235,8 @@ snapshots:
12608 13235
12609 is-number@7.0.0: {} 13236 is-number@7.0.0: {}
12610 13237
13238 + is-obj@2.0.0: {}
13239 +
12611 is-object@1.0.2: {} 13240 is-object@1.0.2: {}
12612 13241
12613 is-path-inside@3.0.3: {} 13242 is-path-inside@3.0.3: {}
...@@ -12654,6 +13283,10 @@ snapshots: ...@@ -12654,6 +13283,10 @@ snapshots:
12654 has-symbols: 1.1.0 13283 has-symbols: 1.1.0
12655 safe-regex-test: 1.1.0 13284 safe-regex-test: 1.1.0
12656 13285
13286 + is-text-path@1.0.1:
13287 + dependencies:
13288 + text-extensions: 1.9.0
13289 +
12657 is-typed-array@1.1.15: 13290 is-typed-array@1.1.15:
12658 dependencies: 13291 dependencies:
12659 which-typed-array: 1.1.20 13292 which-typed-array: 1.1.20
...@@ -12807,6 +13440,8 @@ snapshots: ...@@ -12807,6 +13440,8 @@ snapshots:
12807 13440
12808 json-buffer@3.0.1: {} 13441 json-buffer@3.0.1: {}
12809 13442
13443 + json-parse-better-errors@1.0.2: {}
13444 +
12810 json-parse-even-better-errors@2.3.1: {} 13445 json-parse-even-better-errors@2.3.1: {}
12811 13446
12812 json-schema-traverse@0.4.1: {} 13447 json-schema-traverse@0.4.1: {}
...@@ -12815,6 +13450,8 @@ snapshots: ...@@ -12815,6 +13450,8 @@ snapshots:
12815 13450
12816 json-stable-stringify-without-jsonify@1.0.1: {} 13451 json-stable-stringify-without-jsonify@1.0.1: {}
12817 13452
13453 + json-stringify-safe@5.0.1: {}
13454 +
12818 json5@1.0.2: 13455 json5@1.0.2:
12819 dependencies: 13456 dependencies:
12820 minimist: 1.2.8 13457 minimist: 1.2.8
...@@ -12835,6 +13472,8 @@ snapshots: ...@@ -12835,6 +13472,8 @@ snapshots:
12835 dependencies: 13472 dependencies:
12836 object-assign: 4.1.1 13473 object-assign: 4.1.1
12837 13474
13475 + jsonparse@1.3.1: {}
13476 +
12838 jsx-ast-utils@3.3.5: 13477 jsx-ast-utils@3.3.5:
12839 dependencies: 13478 dependencies:
12840 array-includes: 3.1.9 13479 array-includes: 3.1.9
...@@ -13026,6 +13665,13 @@ snapshots: ...@@ -13026,6 +13665,13 @@ snapshots:
13026 rfdc: 1.4.1 13665 rfdc: 1.4.1
13027 wrap-ansi: 9.0.2 13666 wrap-ansi: 9.0.2
13028 13667
13668 + load-json-file@4.0.0:
13669 + dependencies:
13670 + graceful-fs: 4.2.11
13671 + parse-json: 4.0.0
13672 + pify: 3.0.0
13673 + strip-bom: 3.0.0
13674 +
13029 loader-runner@4.3.1: {} 13675 loader-runner@4.3.1: {}
13030 13676
13031 loader-utils@1.4.2: 13677 loader-utils@1.4.2:
...@@ -13055,6 +13701,11 @@ snapshots: ...@@ -13055,6 +13701,11 @@ snapshots:
13055 pkg-types: 2.3.0 13701 pkg-types: 2.3.0
13056 quansync: 0.2.11 13702 quansync: 0.2.11
13057 13703
13704 + locate-path@2.0.0:
13705 + dependencies:
13706 + p-locate: 2.0.0
13707 + path-exists: 3.0.0
13708 +
13058 locate-path@3.0.0: 13709 locate-path@3.0.0:
13059 dependencies: 13710 dependencies:
13060 p-locate: 3.0.0 13711 p-locate: 3.0.0
...@@ -13074,6 +13725,8 @@ snapshots: ...@@ -13074,6 +13725,8 @@ snapshots:
13074 13725
13075 lodash.debounce@4.0.8: {} 13726 lodash.debounce@4.0.8: {}
13076 13727
13728 + lodash.ismatch@4.4.0: {}
13729 +
13077 lodash.memoize@4.1.2: {} 13730 lodash.memoize@4.1.2: {}
13078 13731
13079 lodash.merge@4.6.2: {} 13732 lodash.merge@4.6.2: {}
...@@ -13127,6 +13780,10 @@ snapshots: ...@@ -13127,6 +13780,10 @@ snapshots:
13127 dependencies: 13780 dependencies:
13128 yallist: 3.1.1 13781 yallist: 3.1.1
13129 13782
13783 + lru-cache@6.0.0:
13784 + dependencies:
13785 + yallist: 4.0.0
13786 +
13130 magic-string@0.30.21: 13787 magic-string@0.30.21:
13131 dependencies: 13788 dependencies:
13132 '@jridgewell/sourcemap-codec': 1.5.5 13789 '@jridgewell/sourcemap-codec': 1.5.5
...@@ -13144,6 +13801,10 @@ snapshots: ...@@ -13144,6 +13801,10 @@ snapshots:
13144 dependencies: 13801 dependencies:
13145 semver: 6.3.1 13802 semver: 6.3.1
13146 13803
13804 + map-obj@1.0.1: {}
13805 +
13806 + map-obj@4.3.0: {}
13807 +
13147 math-intrinsics@1.1.0: {} 13808 math-intrinsics@1.1.0: {}
13148 13809
13149 md5@2.3.0: 13810 md5@2.3.0:
...@@ -13164,6 +13825,20 @@ snapshots: ...@@ -13164,6 +13825,20 @@ snapshots:
13164 dependencies: 13825 dependencies:
13165 fs-monkey: 1.1.0 13826 fs-monkey: 1.1.0
13166 13827
13828 + meow@8.1.2:
13829 + dependencies:
13830 + '@types/minimist': 1.2.5
13831 + camelcase-keys: 6.2.2
13832 + decamelize-keys: 1.1.1
13833 + hard-rejection: 2.1.0
13834 + minimist-options: 4.1.0
13835 + normalize-package-data: 3.0.3
13836 + read-pkg-up: 7.0.1
13837 + redent: 3.0.0
13838 + trim-newlines: 3.0.1
13839 + type-fest: 0.18.1
13840 + yargs-parser: 20.2.9
13841 +
13167 merge-descriptors@1.0.3: {} 13842 merge-descriptors@1.0.3: {}
13168 13843
13169 merge-stream@2.0.0: {} 13844 merge-stream@2.0.0: {}
...@@ -13197,6 +13872,8 @@ snapshots: ...@@ -13197,6 +13872,8 @@ snapshots:
13197 13872
13198 mimic-response@1.0.1: {} 13873 mimic-response@1.0.1: {}
13199 13874
13875 + min-indent@1.0.1: {}
13876 +
13200 mini-css-extract-plugin@2.10.0(webpack@5.91.0(@swc/core@1.3.96)): 13877 mini-css-extract-plugin@2.10.0(webpack@5.91.0(@swc/core@1.3.96)):
13201 dependencies: 13878 dependencies:
13202 schema-utils: 4.3.3 13879 schema-utils: 4.3.3
...@@ -13229,6 +13906,12 @@ snapshots: ...@@ -13229,6 +13906,12 @@ snapshots:
13229 dependencies: 13906 dependencies:
13230 brace-expansion: 2.0.2 13907 brace-expansion: 2.0.2
13231 13908
13909 + minimist-options@4.1.0:
13910 + dependencies:
13911 + arrify: 1.0.1
13912 + is-plain-obj: 1.1.0
13913 + kind-of: 6.0.3
13914 +
13232 minimist@1.2.8: {} 13915 minimist@1.2.8: {}
13233 13916
13234 minipass@6.0.2: {} 13917 minipass@6.0.2: {}
...@@ -13264,6 +13947,8 @@ snapshots: ...@@ -13264,6 +13947,8 @@ snapshots:
13264 13947
13265 mobile-detect@1.4.5: {} 13948 mobile-detect@1.4.5: {}
13266 13949
13950 + modify-values@1.0.1: {}
13951 +
13267 ms@2.0.0: {} 13952 ms@2.0.0: {}
13268 13953
13269 ms@2.1.3: {} 13954 ms@2.1.3: {}
...@@ -13324,6 +14009,20 @@ snapshots: ...@@ -13324,6 +14009,20 @@ snapshots:
13324 dependencies: 14009 dependencies:
13325 abbrev: 2.0.0 14010 abbrev: 2.0.0
13326 14011
14012 + normalize-package-data@2.5.0:
14013 + dependencies:
14014 + hosted-git-info: 2.8.9
14015 + resolve: 1.22.11
14016 + semver: 5.7.2
14017 + validate-npm-package-license: 3.0.4
14018 +
14019 + normalize-package-data@3.0.3:
14020 + dependencies:
14021 + hosted-git-info: 4.1.0
14022 + is-core-module: 2.16.1
14023 + semver: 7.7.4
14024 + validate-npm-package-license: 3.0.4
14025 +
13327 normalize-path@3.0.0: {} 14026 normalize-path@3.0.0: {}
13328 14027
13329 normalize-url@2.0.1: 14028 normalize-url@2.0.1:
...@@ -13474,6 +14173,10 @@ snapshots: ...@@ -13474,6 +14173,10 @@ snapshots:
13474 14173
13475 p-is-promise@1.1.0: {} 14174 p-is-promise@1.1.0: {}
13476 14175
14176 + p-limit@1.3.0:
14177 + dependencies:
14178 + p-try: 1.0.0
14179 +
13477 p-limit@2.3.0: 14180 p-limit@2.3.0:
13478 dependencies: 14181 dependencies:
13479 p-try: 2.2.0 14182 p-try: 2.2.0
...@@ -13486,6 +14189,10 @@ snapshots: ...@@ -13486,6 +14189,10 @@ snapshots:
13486 dependencies: 14189 dependencies:
13487 yocto-queue: 1.2.2 14190 yocto-queue: 1.2.2
13488 14191
14192 + p-locate@2.0.0:
14193 + dependencies:
14194 + p-limit: 1.3.0
14195 +
13489 p-locate@3.0.0: 14196 p-locate@3.0.0:
13490 dependencies: 14197 dependencies:
13491 p-limit: 2.3.0 14198 p-limit: 2.3.0
...@@ -13507,6 +14214,8 @@ snapshots: ...@@ -13507,6 +14214,8 @@ snapshots:
13507 dependencies: 14214 dependencies:
13508 p-finally: 1.0.0 14215 p-finally: 1.0.0
13509 14216
14217 + p-try@1.0.0: {}
14218 +
13510 p-try@2.2.0: {} 14219 p-try@2.2.0: {}
13511 14220
13512 package-json-from-dist@1.0.1: {} 14221 package-json-from-dist@1.0.1: {}
...@@ -13531,6 +14240,11 @@ snapshots: ...@@ -13531,6 +14240,11 @@ snapshots:
13531 dependencies: 14240 dependencies:
13532 callsites: 3.1.0 14241 callsites: 3.1.0
13533 14242
14243 + parse-json@4.0.0:
14244 + dependencies:
14245 + error-ex: 1.3.4
14246 + json-parse-better-errors: 1.0.2
14247 +
13534 parse-json@5.2.0: 14248 parse-json@5.2.0:
13535 dependencies: 14249 dependencies:
13536 '@babel/code-frame': 7.29.0 14250 '@babel/code-frame': 7.29.0
...@@ -13585,6 +14299,10 @@ snapshots: ...@@ -13585,6 +14299,10 @@ snapshots:
13585 14299
13586 path-to-regexp@6.3.0: {} 14300 path-to-regexp@6.3.0: {}
13587 14301
14302 + path-type@3.0.0:
14303 + dependencies:
14304 + pify: 3.0.0
14305 +
13588 path-type@4.0.0: {} 14306 path-type@4.0.0: {}
13589 14307
13590 path-type@6.0.0: {} 14308 path-type@6.0.0: {}
...@@ -14225,6 +14943,8 @@ snapshots: ...@@ -14225,6 +14943,8 @@ snapshots:
14225 14943
14226 punycode@2.3.1: {} 14944 punycode@2.3.1: {}
14227 14945
14946 + q@1.5.1: {}
14947 +
14228 qrcode@1.5.4: 14948 qrcode@1.5.4:
14229 dependencies: 14949 dependencies:
14230 dijkstrajs: 1.0.3 14950 dijkstrajs: 1.0.3
...@@ -14253,6 +14973,8 @@ snapshots: ...@@ -14253,6 +14973,8 @@ snapshots:
14253 14973
14254 queue-microtask@1.2.3: {} 14974 queue-microtask@1.2.3: {}
14255 14975
14976 + quick-lru@4.0.1: {}
14977 +
14256 randombytes@2.1.0: 14978 randombytes@2.1.0:
14257 dependencies: 14979 dependencies:
14258 safe-buffer: 5.2.1 14980 safe-buffer: 5.2.1
...@@ -14290,6 +15012,30 @@ snapshots: ...@@ -14290,6 +15012,30 @@ snapshots:
14290 dependencies: 15012 dependencies:
14291 pify: 2.3.0 15013 pify: 2.3.0
14292 15014
15015 + read-pkg-up@3.0.0:
15016 + dependencies:
15017 + find-up: 2.1.0
15018 + read-pkg: 3.0.0
15019 +
15020 + read-pkg-up@7.0.1:
15021 + dependencies:
15022 + find-up: 4.1.0
15023 + read-pkg: 5.2.0
15024 + type-fest: 0.8.1
15025 +
15026 + read-pkg@3.0.0:
15027 + dependencies:
15028 + load-json-file: 4.0.0
15029 + normalize-package-data: 2.5.0
15030 + path-type: 3.0.0
15031 +
15032 + read-pkg@5.2.0:
15033 + dependencies:
15034 + '@types/normalize-package-data': 2.4.4
15035 + normalize-package-data: 2.5.0
15036 + parse-json: 5.2.0
15037 + type-fest: 0.6.0
15038 +
14293 readable-stream@2.3.8: 15039 readable-stream@2.3.8:
14294 dependencies: 15040 dependencies:
14295 core-util-is: 1.0.3 15041 core-util-is: 1.0.3
...@@ -14314,6 +15060,11 @@ snapshots: ...@@ -14314,6 +15060,11 @@ snapshots:
14314 15060
14315 readdirp@5.0.0: {} 15061 readdirp@5.0.0: {}
14316 15062
15063 + redent@3.0.0:
15064 + dependencies:
15065 + indent-string: 4.0.0
15066 + strip-indent: 3.0.0
15067 +
14317 reflect.getprototypeof@1.0.10: 15068 reflect.getprototypeof@1.0.10:
14318 dependencies: 15069 dependencies:
14319 call-bind: 1.0.8 15070 call-bind: 1.0.8
...@@ -14773,6 +15524,20 @@ snapshots: ...@@ -14773,6 +15524,20 @@ snapshots:
14773 15524
14774 source-map@0.7.6: {} 15525 source-map@0.7.6: {}
14775 15526
15527 + spdx-correct@3.2.0:
15528 + dependencies:
15529 + spdx-expression-parse: 3.0.1
15530 + spdx-license-ids: 3.0.22
15531 +
15532 + spdx-exceptions@2.5.0: {}
15533 +
15534 + spdx-expression-parse@3.0.1:
15535 + dependencies:
15536 + spdx-exceptions: 2.5.0
15537 + spdx-license-ids: 3.0.22
15538 +
15539 + spdx-license-ids@3.0.22: {}
15540 +
14776 spdy-transport@3.0.0: 15541 spdy-transport@3.0.0:
14777 dependencies: 15542 dependencies:
14778 debug: 4.4.3 15543 debug: 4.4.3
...@@ -14798,8 +15563,33 @@ snapshots: ...@@ -14798,8 +15563,33 @@ snapshots:
14798 15563
14799 split-on-first@3.0.0: {} 15564 split-on-first@3.0.0: {}
14800 15565
15566 + split2@3.2.2:
15567 + dependencies:
15568 + readable-stream: 3.6.2
15569 +
15570 + split@1.0.1:
15571 + dependencies:
15572 + through: 2.3.8
15573 +
14801 stackback@0.0.2: {} 15574 stackback@0.0.2: {}
14802 15575
15576 + standard-version@9.5.0:
15577 + dependencies:
15578 + chalk: 2.4.2
15579 + conventional-changelog: 3.1.25
15580 + conventional-changelog-config-spec: 2.1.0
15581 + conventional-changelog-conventionalcommits: 4.6.3
15582 + conventional-recommended-bump: 6.1.0
15583 + detect-indent: 6.1.0
15584 + detect-newline: 3.1.0
15585 + dotgitignore: 2.1.0
15586 + figures: 3.2.0
15587 + find-up: 5.0.0
15588 + git-semver-tags: 4.1.1
15589 + semver: 7.7.4
15590 + stringify-package: 1.0.1
15591 + yargs: 16.2.0
15592 +
14803 statuses@1.5.0: {} 15593 statuses@1.5.0: {}
14804 15594
14805 statuses@2.0.2: {} 15595 statuses@2.0.2: {}
...@@ -14892,6 +15682,8 @@ snapshots: ...@@ -14892,6 +15682,8 @@ snapshots:
14892 dependencies: 15682 dependencies:
14893 safe-buffer: 5.2.1 15683 safe-buffer: 5.2.1
14894 15684
15685 + stringify-package@1.0.1: {}
15686 +
14895 strip-ansi@6.0.1: 15687 strip-ansi@6.0.1:
14896 dependencies: 15688 dependencies:
14897 ansi-regex: 5.0.1 15689 ansi-regex: 5.0.1
...@@ -14910,6 +15702,10 @@ snapshots: ...@@ -14910,6 +15702,10 @@ snapshots:
14910 15702
14911 strip-final-newline@3.0.0: {} 15703 strip-final-newline@3.0.0: {}
14912 15704
15705 + strip-indent@3.0.0:
15706 + dependencies:
15707 + min-indent: 1.0.1
15708 +
14913 strip-json-comments@2.0.1: {} 15709 strip-json-comments@2.0.1: {}
14914 15710
14915 strip-json-comments@3.1.1: {} 15711 strip-json-comments@3.1.1: {}
...@@ -14970,6 +15766,10 @@ snapshots: ...@@ -14970,6 +15766,10 @@ snapshots:
14970 dependencies: 15766 dependencies:
14971 copy-anything: 4.0.5 15767 copy-anything: 4.0.5
14972 15768
15769 + supports-color@5.5.0:
15770 + dependencies:
15771 + has-flag: 3.0.0
15772 +
14973 supports-color@7.2.0: 15773 supports-color@7.2.0:
14974 dependencies: 15774 dependencies:
14975 has-flag: 4.0.0 15775 has-flag: 4.0.0
...@@ -15090,6 +15890,8 @@ snapshots: ...@@ -15090,6 +15890,8 @@ snapshots:
15090 commander: 2.20.3 15890 commander: 2.20.3
15091 source-map-support: 0.5.21 15891 source-map-support: 0.5.21
15092 15892
15893 + text-extensions@1.9.0: {}
15894 +
15093 text-table@0.2.0: {} 15895 text-table@0.2.0: {}
15094 15896
15095 thenify-all@1.6.0: 15897 thenify-all@1.6.0:
...@@ -15100,6 +15902,15 @@ snapshots: ...@@ -15100,6 +15902,15 @@ snapshots:
15100 dependencies: 15902 dependencies:
15101 any-promise: 1.3.0 15903 any-promise: 1.3.0
15102 15904
15905 + through2@2.0.5:
15906 + dependencies:
15907 + readable-stream: 2.3.8
15908 + xtend: 4.0.2
15909 +
15910 + through2@4.0.2:
15911 + dependencies:
15912 + readable-stream: 3.6.2
15913 +
15103 through@2.3.8: {} 15914 through@2.3.8: {}
15104 15915
15105 thunky@1.1.0: {} 15916 thunky@1.1.0: {}
...@@ -15148,6 +15959,8 @@ snapshots: ...@@ -15148,6 +15959,8 @@ snapshots:
15148 dependencies: 15959 dependencies:
15149 punycode: 2.3.1 15960 punycode: 2.3.1
15150 15961
15962 + trim-newlines@3.0.1: {}
15963 +
15151 trim-repeated@1.0.0: 15964 trim-repeated@1.0.0:
15152 dependencies: 15965 dependencies:
15153 escape-string-regexp: 1.0.5 15966 escape-string-regexp: 1.0.5
...@@ -15181,10 +15994,16 @@ snapshots: ...@@ -15181,10 +15994,16 @@ snapshots:
15181 15994
15182 type-detect@4.1.0: {} 15995 type-detect@4.1.0: {}
15183 15996
15997 + type-fest@0.18.1: {}
15998 +
15184 type-fest@0.20.2: {} 15999 type-fest@0.20.2: {}
15185 16000
15186 type-fest@0.21.3: {} 16001 type-fest@0.21.3: {}
15187 16002
16003 + type-fest@0.6.0: {}
16004 +
16005 + type-fest@0.8.1: {}
16006 +
15188 type-fest@2.19.0: {} 16007 type-fest@2.19.0: {}
15189 16008
15190 type-is@1.6.18: 16009 type-is@1.6.18:
...@@ -15225,6 +16044,8 @@ snapshots: ...@@ -15225,6 +16044,8 @@ snapshots:
15225 possible-typed-array-names: 1.1.0 16044 possible-typed-array-names: 1.1.0
15226 reflect.getprototypeof: 1.0.10 16045 reflect.getprototypeof: 1.0.10
15227 16046
16047 + typedarray@0.0.6: {}
16048 +
15228 typescript@5.9.3: {} 16049 typescript@5.9.3: {}
15229 16050
15230 ufo@1.6.3: {} 16051 ufo@1.6.3: {}
...@@ -15339,6 +16160,11 @@ snapshots: ...@@ -15339,6 +16160,11 @@ snapshots:
15339 16160
15340 validate-html-nesting@1.2.4: {} 16161 validate-html-nesting@1.2.4: {}
15341 16162
16163 + validate-npm-package-license@3.0.4:
16164 + dependencies:
16165 + spdx-correct: 3.2.0
16166 + spdx-expression-parse: 3.0.1
16167 +
15342 validate-npm-package-name@5.0.1: {} 16168 validate-npm-package-name@5.0.1: {}
15343 16169
15344 vary@1.1.2: {} 16170 vary@1.1.2: {}
...@@ -15705,6 +16531,8 @@ snapshots: ...@@ -15705,6 +16531,8 @@ snapshots:
15705 16531
15706 word-wrap@1.2.5: {} 16532 word-wrap@1.2.5: {}
15707 16533
16534 + wordwrap@1.0.0: {}
16535 +
15708 wrap-ansi@6.2.0: 16536 wrap-ansi@6.2.0:
15709 dependencies: 16537 dependencies:
15710 ansi-styles: 4.3.0 16538 ansi-styles: 4.3.0
...@@ -15753,6 +16581,8 @@ snapshots: ...@@ -15753,6 +16581,8 @@ snapshots:
15753 16581
15754 yallist@3.1.1: {} 16582 yallist@3.1.1: {}
15755 16583
16584 + yallist@4.0.0: {}
16585 +
15756 yaml@2.8.2: {} 16586 yaml@2.8.2: {}
15757 16587
15758 yargs-parser@18.1.3: 16588 yargs-parser@18.1.3:
......
1 +#!/bin/bash
2 +
3 +# CHANGELOG 归档脚本
4 +# 当 CHANGELOG.md 超过 20 条记录时,自动归档旧记录
5 +
6 +CHANGELOG_FILE="docs/CHANGELOG.md"
7 +ARCHIVE_DIR="docs/changelog-archive"
8 +MAX_ENTRIES=20
9 +
10 +# 检查主文件是否存在
11 +if [ ! -f "$CHANGELOG_FILE" ]; then
12 + echo "❌ CHANGELOG.md 文件不存在"
13 + exit 1
14 +fi
15 +
16 +# 创建归档目录
17 +mkdir -p "$ARCHIVE_DIR"
18 +
19 +# 统计当前记录数
20 +ENTRY_COUNT=$(grep -c "^## \[" "$CHANGELOG_FILE")
21 +
22 +echo "📊 当前 CHANGELOG.md 记录数: $ENTRY_COUNT"
23 +
24 +# 如果记录数超过阈值,执行归档
25 +if [ "$ENTRY_COUNT" -gt "$MAX_ENTRIES" ]; then
26 + echo "⚠️ 记录数超过 $MAX_ENTRIES 条,开始归档..."
27 +
28 + # 找到第 (MAX_ENTRIES + 1) 条记录的起始行
29 + SPLIT_LINE=$(grep -n "^## \[" "$CHANGELOG_FILE" | sed -n "$((MAX_ENTRIES + 1))p" | cut -d: -f1)
30 +
31 + if [ -z "$SPLIT_LINE" ]; then
32 + echo "❌ 无法找到分割点"
33 + exit 1
34 + fi
35 +
36 + # 生成归档文件名(带日期)
37 + ARCHIVE_FILE="$ARCHIVE_DIR/CHANGELOG-archive-$(date +%Y%m%d).md"
38 +
39 + # 移动旧记录到归档文件
40 + tail -n +"$SPLIT_LINE" "$CHANGELOG_FILE" > "$ARCHIVE_FILE"
41 +
42 + # 只保留前 MAX_ENTRIES 条记录
43 + head -n "$((SPLIT_LINE - 1))" "$CHANGELOG_FILE" > "$CHANGELOG_FILE.tmp"
44 + mv "$CHANGELOG_FILE.tmp" "$CHANGELOG_FILE"
45 +
46 + NEW_COUNT=$(grep -c "^## \[" "$CHANGELOG_FILE")
47 + ARCHIVE_COUNT=$(grep -c "^## \[" "$ARCHIVE_FILE")
48 +
49 + echo "✅ 归档完成"
50 + echo " 主文件记录数: $NEW_COUNT"
51 + echo " 归档文件记录数: $ARCHIVE_COUNT"
52 + echo " 归档文件: $ARCHIVE_FILE"
53 +
54 + # 显示文件大小
55 + echo ""
56 + echo "📏 文件大小:"
57 + ls -lh "$CHANGELOG_FILE" "$ARCHIVE_FILE"
58 +else
59 + echo "✅ 记录数 ($ENTRY_COUNT) 未超过阈值 ($MAX_ENTRIES),无需归档"
60 +fi
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
13 :key="option" 13 :key="option"
14 :label="option" 14 :label="option"
15 class="mr-8" 15 class="mr-8"
16 + @change="() => emit('change', option)"
16 > 17 >
17 {{ option }} 18 {{ option }}
18 </nut-radio> 19 </nut-radio>
...@@ -89,7 +90,13 @@ const emit = defineEmits([ ...@@ -89,7 +90,13 @@ const emit = defineEmits([
89 * @event update:modelValue 90 * @event update:modelValue
90 * @param {string} value - 选中的选项 91 * @param {string} value - 选中的选项
91 */ 92 */
92 - 'update:modelValue' 93 + 'update:modelValue',
94 + /**
95 + * 选项变化事件
96 + * @event change
97 + * @param {string} value - 选中的选项
98 + */
99 + 'change'
93 ]) 100 ])
94 101
95 /** 102 /**
......
...@@ -49,6 +49,7 @@ import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue ...@@ -49,6 +49,7 @@ import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue
49 import SavingsTemplate from './PlanTemplates/SavingsTemplate.vue' 49 import SavingsTemplate from './PlanTemplates/SavingsTemplate.vue'
50 import { PLAN_TEMPLATES } from '@/config/plan-templates' 50 import { PLAN_TEMPLATES } from '@/config/plan-templates'
51 import { addAPI } from '@/api/plan' 51 import { addAPI } from '@/api/plan'
52 +import { useFieldValueTransform } from '@/composables/useFieldValueTransform'
52 53
53 /** 54 /**
54 * 组件属性 55 * 组件属性
...@@ -237,7 +238,11 @@ const close = async () => { ...@@ -237,7 +238,11 @@ const close = async () => {
237 console.log('[PlanFormContainer] 弹窗已关闭,表单已重置') 238 console.log('[PlanFormContainer] 弹窗已关闭,表单已重置')
238 } 239 }
239 240
240 -// 提交表单 - 将表单数据和产品信息提交到后端 API 241 +/**
242 + * 提交表单
243 + * @description 将表单数据与产品信息组装后提交到后端
244 + * @returns {Promise<boolean>} 是否提交成功
245 + */
241 const submit = async () => { 246 const submit = async () => {
242 if (!props.product) { 247 if (!props.product) {
243 console.error('[PlanFormContainer] 无法提交: 产品数据为空') 248 console.error('[PlanFormContainer] 无法提交: 产品数据为空')
...@@ -264,23 +269,24 @@ const submit = async () => { ...@@ -264,23 +269,24 @@ const submit = async () => {
264 }) 269 })
265 270
266 try { 271 try {
267 - // 字段名映射:将表单字段名映射为 API 期望的字段名 272 + // 默认字段映射:模板未提供 submit_mapping 时使用
268 - // 根据 API 文档 (docs/api-specs/plan/add.md) 定义 273 + const defaultMapping = {
269 - const fieldMapping = { 274 + customer_name: { api_field: 'customer_name' },
270 - customer_name: 'customer_name', // 申请人(已直接使用) 275 + gender: { api_field: 'customer_gender' },
271 - gender: 'customer_gender', // 性别 → customer_gender 276 + birthday: { api_field: 'customer_birthday' },
272 - birthday: 'customer_birthday', // 出生年月日 → customer_birthday 277 + smoker: { api_field: 'smoking_status' },
273 - smoker: 'smoking_status', // 是否吸烟 → smoking_status 278 + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
274 - coverage: 'annual_premium', // 保额/年缴保费 → annual_premium 279 + payment_period: { api_field: 'payment_years' },
275 - payment_period: 'payment_years', // 缴费年期 → payment_years 280 + withdrawal_enabled: { api_field: 'allow_reduce_amount' },
276 - withdrawal_enabled: 'allow_reduce_amount', // 是否容许减少名义金额 281 + withdrawal_mode: { api_field: 'withdrawal_option' },
277 - withdrawal_mode: 'withdrawal_option', // 提取选项 282 + withdrawal_method: { api_field: 'withdrawal_method' },
278 - withdrawal_start_age: 'withdrawal_start_age', // 提取开始年龄 283 + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
279 - withdrawal_period: 'withdrawal_period', // 提取期 284 + annual_increase_percentage: { api_field: 'annual_increase_percentage' },
280 - currency_type: 'currency_type', // 币种类型 285 + withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' },
281 - // 新增字段映射 286 + withdrawal_period_specified: { api_field: 'withdrawal_period' },
282 - annual_withdrawal_amount: 'annual_withdrawal_amount', // 每年提取金额 287 + withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' },
283 - annual_increase_percentage: 'annual_increase_percentage' // 每年递增提取百分比 288 + withdrawal_period_fixed: { api_field: 'withdrawal_period' },
289 + total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' }
284 } 290 }
285 291
286 // 构建请求数据 292 // 构建请求数据
...@@ -289,46 +295,53 @@ const submit = async () => { ...@@ -289,46 +295,53 @@ const submit = async () => {
289 } 295 }
290 296
291 // 映射表单字段到 API 字段 297 // 映射表单字段到 API 字段
298 + const submitMapping = templateConfig.value?.config?.submit_mapping || defaultMapping
299 + const { toYuan } = useFieldValueTransform(formData, submitMapping)
300 +
292 Object.keys(formData.value).forEach(key => { 301 Object.keys(formData.value).forEach(key => {
293 - const apiField = fieldMapping[key] 302 + const mapping = submitMapping[key]
294 - 303 + if (mapping) {
295 - if (apiField) { 304 + const apiField = typeof mapping === 'string' ? mapping : mapping.api_field
296 - // 有映射:使用映射后的字段名 305 + let value = formData.value[key]
297 - // 特殊处理:coverage(分)需要转换为元 306 + // 金额字段从分转换为元
298 - if (key === 'coverage') { 307 + if (typeof mapping === 'object' && mapping.transform === 'fen_to_yuan' && value !== null && value !== undefined && value !== '') {
299 - const coverageInYuan = (formData.value[key] / 100).toFixed(2) 308 + value = toYuan(key, value)
300 - console.log(`[PlanFormContainer] coverage 转换: ${key} (${formData.value[key]} ) ${apiField} (${coverageInYuan} )`)
301 - requestData[apiField] = coverageInYuan
302 - }
303 - // 特殊处理:annual_withdrawal_amount(分)需要转换为元
304 - else if (key === 'annual_withdrawal_amount') {
305 - const amountInYuan = (formData.value[key] / 100).toFixed(2)
306 - console.log(`[PlanFormContainer] annual_withdrawal_amount 转换: ${key} (${formData.value[key]} ) ${apiField} (${amountInYuan} )`)
307 - requestData[apiField] = amountInYuan
308 } 309 }
309 - // 特殊处理:annual_increase_percentage(直接传递,已是字符串) 310 + requestData[apiField] = value
310 - else if (key === 'annual_increase_percentage') {
311 - requestData[apiField] = formData.value[key]
312 - }
313 - else {
314 - requestData[apiField] = formData.value[key]
315 - }
316 - } else if (key === 'total_amount') {
317 - // 特殊处理:总保费(分 → 元)
318 - requestData.total_premium = (formData.value[key] / 100).toFixed(2)
319 } else { 311 } else {
320 - // 无映射:保持原字段名
321 requestData[key] = formData.value[key] 312 requestData[key] = formData.value[key]
322 } 313 }
323 }) 314 })
324 315
316 + if (formData.value?.withdrawal_mode === '指定提取金额') {
317 + const specifiedStart = formData.value.withdrawal_start_age_specified
318 + const specifiedPeriod = formData.value.withdrawal_period_specified
319 + if (specifiedStart !== undefined && specifiedStart !== null && specifiedStart !== '') {
320 + requestData.withdrawal_start_age = specifiedStart
321 + }
322 + if (specifiedPeriod !== undefined && specifiedPeriod !== null && specifiedPeriod !== '') {
323 + requestData.withdrawal_period = specifiedPeriod
324 + }
325 + }
326 +
327 + if (formData.value?.withdrawal_mode === '最高固定提取金额') {
328 + const fixedStart = formData.value.withdrawal_start_age_fixed
329 + const fixedPeriod = formData.value.withdrawal_period_fixed
330 + if (fixedStart !== undefined && fixedStart !== null && fixedStart !== '') {
331 + requestData.withdrawal_start_age = fixedStart
332 + }
333 + if (fixedPeriod !== undefined && fixedPeriod !== null && fixedPeriod !== '') {
334 + requestData.withdrawal_period = fixedPeriod
335 + }
336 + }
337 +
325 // 添加币种类型(如果有配置) 338 // 添加币种类型(如果有配置)
326 if (templateConfig.value?.config?.currency) { 339 if (templateConfig.value?.config?.currency) {
327 requestData.currency_type = templateConfig.value.config.currency 340 requestData.currency_type = templateConfig.value.config.currency
328 } 341 }
329 342
330 console.log('[PlanFormContainer] 提交计划书请求数据:', requestData) 343 console.log('[PlanFormContainer] 提交计划书请求数据:', requestData)
331 - console.log('[PlanFormContainer] 字段映射:', fieldMapping) 344 + console.log('[PlanFormContainer] 字段映射:', submitMapping)
332 345
333 // 调用 API 346 // 调用 API
334 const res = await addAPI(requestData) 347 const res = await addAPI(requestData)
......
1 <template> 1 <template>
2 <div v-if="config"> 2 <div v-if="config">
3 - <!-- 申请人 --> 3 + <template v-for="field in baseFields" :key="field.id || field.key">
4 - <PlanFieldName 4 + <component
5 - v-model="form.customer_name" 5 + v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
6 - label="申请人" 6 + :is="getFieldComponent(field)"
7 - placeholder="请输入申请人" 7 + v-model="form[field.key]"
8 - :required="true" 8 + v-bind="getFieldProps(field)"
9 class="mb-5" 9 class="mb-5"
10 /> 10 />
11 - 11 + <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
12 - <!-- 性别 --> 12 + <div class="text-sm text-gray-700 mb-2 flex items-center">
13 - <PlanFieldRadio 13 + <span v-if="field.required" class="text-red-500 mr-1">*</span>
14 - v-model="form.gender" 14 + <span>{{ field.label }}</span>
15 - label="性别" 15 + </div>
16 - :options="['男', '女']" 16 + <nut-input
17 - :required="true" 17 + v-model="form[field.key]"
18 - class="mb-5" 18 + type="digit"
19 - /> 19 + :placeholder="field.placeholder"
20 - 20 + @input="(value) => onPercentageInput(value, field.key)"
21 - <!-- 出生年月日 --> 21 + class="w-full"
22 - <PlanFieldDatePicker
23 - v-model="form.birthday"
24 - label="出生年月日"
25 - placeholder="请选择年月日"
26 - :required="true"
27 - class="mb-5"
28 - />
29 -
30 - <!-- 是否吸烟 -->
31 - <PlanFieldRadio
32 - v-model="form.smoker"
33 - label="是否吸烟"
34 - :options="['是', '否']"
35 - :required="true"
36 - class="mb-5"
37 - />
38 -
39 - <!-- 保额 -->
40 - <PlanFieldAmount
41 - v-model="form.coverage"
42 - label="保额"
43 - placeholder="请输入保额"
44 - :input-label="'请输入保额金额'"
45 - :currency="config.currency"
46 - :required="true"
47 - class="mb-5"
48 - />
49 -
50 - <!-- 缴费年期 - 单选形式 -->
51 - <PaymentPeriodRadio
52 - v-model="form.payment_period"
53 - label="缴费年期"
54 - :options="config.payment_periods"
55 - :required="true"
56 - class="mb-5"
57 /> 22 />
58 </div> 23 </div>
24 + </template>
25 + </div>
59 26
60 <!-- 配置缺失提示 --> 27 <!-- 配置缺失提示 -->
61 <div v-else class="text-center text-gray-500 py-10"> 28 <div v-else class="text-center text-gray-500 py-10">
...@@ -77,13 +44,14 @@ ...@@ -77,13 +44,14 @@
77 * :config="templateConfig" 44 * :config="templateConfig"
78 * /> 45 * />
79 */ 46 */
80 -import { reactive, watch } from 'vue' 47 +import { reactive, watch, computed } from 'vue'
81 import Taro from '@tarojs/taro' 48 import Taro from '@tarojs/taro'
82 import PlanFieldName from '../PlanFields/NameInput.vue' 49 import PlanFieldName from '../PlanFields/NameInput.vue'
83 import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' 50 import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
84 import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' 51 import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
85 import PlanFieldRadio from '../PlanFields/RadioGroup.vue' 52 import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
86 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' 53 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
54 +import { useFieldDependencies } from '@/composables/useFieldDependencies'
87 55
88 /** 56 /**
89 * 组件属性 57 * 组件属性
...@@ -105,6 +73,7 @@ const props = defineProps({ ...@@ -105,6 +73,7 @@ const props = defineProps({
105 * @property {Array<string>} payment_periods - 缴费年期选项 73 * @property {Array<string>} payment_periods - 缴费年期选项
106 * @property {Object} age_range - 年龄范围 { min, max } 74 * @property {Object} age_range - 年龄范围 { min, max }
107 * @property {string} insurance_period - 保险期间 75 * @property {string} insurance_period - 保险期间
76 + * @property {Object} form_schema - 表单 Schema
108 */ 77 */
109 config: { 78 config: {
110 type: Object, 79 type: Object,
...@@ -137,6 +106,104 @@ const form = reactive({}) ...@@ -137,6 +106,104 @@ const form = reactive({})
137 106
138 let previousModelValue = null 107 let previousModelValue = null
139 108
109 +// 字段类型与组件的对应关系
110 +const fieldComponentMap = {
111 + name: PlanFieldName,
112 + radio: PlanFieldRadio,
113 + date: PlanFieldDatePicker,
114 + amount: PlanFieldAmount,
115 + payment_period: PaymentPeriodRadio
116 +}
117 +
118 +// Schema 配置入口
119 +const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
120 +
121 +const fieldDefinitions = computed(() => {
122 + return baseFields.value.reduce((result, field) => {
123 + result[field.key] = field
124 + return result
125 + }, {})
126 +})
127 +
128 +/**
129 + * 获取字段对应的渲染组件
130 + * @param {Object} field - 字段配置
131 + * @returns {Object|null} Vue 组件
132 + */
133 +const getFieldComponent = (field) => {
134 + return fieldComponentMap[field.type] || null
135 +}
136 +
137 +/**
138 + * 组装字段渲染所需的 props
139 + * @param {Object} field - 字段配置
140 + * @returns {Object} 传入字段组件的 props
141 + */
142 +const getFieldProps = (field) => {
143 + const fieldProps = {
144 + label: field.label,
145 + placeholder: field.placeholder,
146 + required: !!field.required
147 + }
148 +
149 + if (field.options) {
150 + fieldProps.options = field.options
151 + }
152 +
153 + // 缴费年期选项由模板配置提供
154 + if (field.options_from === 'payment_periods') {
155 + fieldProps.options = fieldProps.options || props.config?.payment_periods
156 + }
157 +
158 + // 基础币种来自模板配置
159 + if (field.currency_from === 'currency') {
160 + fieldProps.currency = props.config?.currency
161 + }
162 +
163 + // 金额键盘的弹窗提示文本
164 + if (field.input_label) {
165 + fieldProps.inputLabel = field.input_label
166 + }
167 +
168 + return fieldProps
169 +}
170 +
171 +const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
172 +
173 +/**
174 + * 获取 Schema 默认值
175 + * @param {Object} value - 当前表单数据
176 + * @returns {Object} 默认值集合
177 + */
178 +const getSchemaDefaults = (value) => {
179 + const defaults = {}
180 + const fields = [...baseFields.value]
181 + fields.forEach(field => {
182 + if (field.default !== undefined && (value?.[field.key] === undefined || value?.[field.key] === null)) {
183 + defaults[field.key] = field.default
184 + }
185 + })
186 + return defaults
187 +}
188 +
189 +/**
190 + * 初始化表单数据
191 + * @param {Object} value - 初始数据
192 + */
193 +const initializeForm = (value) => {
194 + if (!value) {
195 + Object.keys(form).forEach(key => delete form[key])
196 + return
197 + }
198 +
199 + const defaults = getSchemaDefaults(value)
200 +
201 + Object.assign(form, {
202 + ...value,
203 + ...defaults
204 + })
205 +}
206 +
140 // 监听父组件的数据变化 207 // 监听父组件的数据变化
141 watch( 208 watch(
142 () => props.modelValue, 209 () => props.modelValue,
...@@ -155,58 +222,120 @@ watch( ...@@ -155,58 +222,120 @@ watch(
155 222
156 if (isReset) { 223 if (isReset) {
157 // 父组件重置了:清空表单 224 // 父组件重置了:清空表单
158 - Object.keys(form).forEach(key => delete form[key]) 225 + initializeForm(newVal)
159 previousModelValue = newVal 226 previousModelValue = newVal
160 } else { 227 } else {
161 - // 正常更新:合并新字段,不删除已有字段 228 + // 正常更新:合并新字段,保留默认值逻辑
162 - // 这很重要!因为用户可能刚填写了某些字段,其他字段还没更新 229 + const defaults = getSchemaDefaults(newVal)
163 - Object.keys(newVal).forEach(key => { 230 + Object.assign(form, {
164 - form[key] = newVal[key] 231 + ...newVal,
232 + ...defaults
165 }) 233 })
166 previousModelValue = newVal 234 previousModelValue = newVal
167 } 235 }
168 }, 236 },
169 - { immediate: true } 237 + { immediate: true, deep: true }
170 ) 238 )
171 239
172 /** 240 /**
173 * 监听表单数据变化,同步到父组件 241 * 监听表单数据变化,同步到父组件
174 */ 242 */
243 +// 监听表单数据变化,同步到父组件
175 watch( 244 watch(
176 - () => form, 245 + form,
177 - (newVal) => emit('update:modelValue', newVal), 246 + (newVal) => emit('update:modelValue', { ...newVal }),
178 { deep: true } 247 { deep: true }
179 ) 248 )
180 249
181 /** 250 /**
182 - * 表单校验 251 + * 百分比输入清洗,避免非法字符
183 - * @returns {boolean} 是否通过校验 252 + * @param {string|number} value - 输入值
253 + * @param {string} key - 目标字段 key
184 */ 254 */
185 -const validate = () => { 255 +const onPercentageInput = (value, key) => {
186 - if (!form.customer_name || !form.customer_name.trim()) { 256 + // 转换为字符串(处理 value 为 null 或其他类型的情况)
187 - Taro.showToast({ title: '请输入申请人', icon: 'none' }) 257 + let strValue = String(value ?? '')
188 - return false 258 +
259 + // 只保留数字和小数点
260 + let cleaned = strValue.replace(/[^\d.]/g, '')
261 +
262 + // 只保留一个小数点
263 + const parts = cleaned.split('.')
264 + if (parts.length > 2) {
265 + cleaned = parts[0] + '.' + parts.slice(1).join('')
189 } 266 }
190 - if (!form.gender) { 267 +
191 - Taro.showToast({ title: '请选择性别', icon: 'none' }) 268 + // 限制小数点后最多 2 位
192 - return false 269 + if (parts.length === 2 && parts[1].length > 2) {
270 + cleaned = parts[0] + '.' + parts[1].slice(0, 2)
271 + }
272 +
273 + // 限制范围:0-100
274 + const numValue = parseFloat(cleaned)
275 + if (!Number.isNaN(numValue)) {
276 + if (numValue > 100) {
277 + cleaned = '100'
278 + } else if (numValue < 0) {
279 + cleaned = '0'
193 } 280 }
194 - if (!form.birthday) {
195 - Taro.showToast({ title: '请选择出生年月日', icon: 'none' })
196 - return false
197 } 281 }
198 - if (!form.smoker) { 282 +
199 - Taro.showToast({ title: '请选择是否吸烟', icon: 'none' }) 283 + form[key] = cleaned
284 +}
285 +
286 +const isEmptyValue = (value) => {
287 + if (value === null || value === undefined) return true
288 + if (typeof value === 'string' && value.trim() === '') return true
289 + if (Array.isArray(value) && value.length === 0) return true
200 return false 290 return false
291 +}
292 +
293 +const getRequiredMessage = (field) => {
294 + if (field?.placeholder) return field.placeholder
295 + const label = field?.label || '必填信息'
296 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
297 + if (selectTypes.includes(field?.type)) {
298 + return `请选择${label}`
201 } 299 }
202 - if (!form.coverage) { 300 + return `请输入${label}`
203 - Taro.showToast({ title: '请输入保额', icon: 'none' }) 301 +}
302 +
303 +const isFieldRequired = (field) => {
304 + return field?.required === true || field?.required === undefined
305 +}
306 +
307 +/**
308 + * 表单校验(基于 Schema)
309 + * @returns {boolean} 校验是否通过
310 + */
311 +const validate = () => {
312 + const fields = [...baseFields.value]
313 +
314 + for (const field of fields) {
315 + if (!isFieldVisible(field.key)) {
316 + continue
317 + }
318 +
319 + if (isFieldRequired(field)) {
320 + const value = form[field.key]
321 + if (isEmptyValue(value)) {
322 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
204 return false 323 return false
205 } 324 }
206 - if (!form.payment_period) { 325 + }
207 - Taro.showToast({ title: '请选择缴费年期', icon: 'none' }) 326 +
327 + if (field.type === 'percentage' && isFieldVisible(field.key)) {
328 + const value = form[field.key]
329 + if (!isEmptyValue(value)) {
330 + const percentage = parseFloat(value)
331 + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
332 + Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
208 return false 333 return false
209 } 334 }
335 + }
336 + }
337 + }
338 +
210 return true 339 return true
211 } 340 }
212 341
......
1 <template> 1 <template>
2 <div v-if="config"> 2 <div v-if="config">
3 - <!-- 申请人 --> 3 + <template v-for="field in baseFields" :key="field.id || field.key">
4 - <PlanFieldName 4 + <component
5 - v-model="form.customer_name" 5 + v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
6 - label="申请人" 6 + :is="getFieldComponent(field)"
7 - placeholder="请输入申请人" 7 + v-model="form[field.key]"
8 - :required="true" 8 + v-bind="getFieldProps(field)"
9 class="mb-5" 9 class="mb-5"
10 /> 10 />
11 - 11 + <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
12 - <!-- 性别 --> 12 + <div class="text-sm text-gray-700 mb-2 flex items-center">
13 - <PlanFieldRadio 13 + <span v-if="field.required" class="text-red-500 mr-1">*</span>
14 - v-model="form.gender" 14 + <span>{{ field.label }}</span>
15 - label="性别" 15 + </div>
16 - :options="['男', '女']" 16 + <nut-input
17 - :required="true" 17 + v-model="form[field.key]"
18 - class="mb-5" 18 + type="digit"
19 - /> 19 + :placeholder="field.placeholder"
20 - 20 + @input="(value) => onPercentageInput(value, field.key)"
21 - <!-- 出生年月日 --> 21 + class="w-full"
22 - <PlanFieldDatePicker
23 - v-model="form.birthday"
24 - label="出生年月日"
25 - placeholder="请选择年月日"
26 - :required="true"
27 - class="mb-5"
28 - />
29 -
30 - <!-- 是否吸烟 -->
31 - <PlanFieldRadio
32 - v-model="form.smoker"
33 - label="是否吸烟"
34 - :options="['是', '否']"
35 - :required="true"
36 - class="mb-5"
37 - />
38 -
39 - <!-- 保额 -->
40 - <PlanFieldAmount
41 - v-model="form.coverage"
42 - label="保额"
43 - placeholder="请输入保额"
44 - :input-label="'请输入保额金额'"
45 - :currency="config.currency"
46 - :required="true"
47 - class="mb-5"
48 - />
49 -
50 - <!-- 缴费年期 - 单选形式 -->
51 - <PaymentPeriodRadio
52 - v-model="form.payment_period"
53 - label="缴费年期"
54 - :options="config.payment_periods"
55 - :required="true"
56 - class="mb-5"
57 /> 22 />
58 </div> 23 </div>
24 + </template>
25 + </div>
59 26
60 <!-- 配置缺失提示 --> 27 <!-- 配置缺失提示 -->
61 <div v-else class="text-center text-gray-500 py-10"> 28 <div v-else class="text-center text-gray-500 py-10">
...@@ -77,13 +44,14 @@ ...@@ -77,13 +44,14 @@
77 * :config="templateConfig" 44 * :config="templateConfig"
78 * /> 45 * />
79 */ 46 */
80 -import { reactive, watch, toRefs } from 'vue' 47 +import { reactive, watch, computed } from 'vue'
81 import Taro from '@tarojs/taro' 48 import Taro from '@tarojs/taro'
82 import PlanFieldName from '../PlanFields/NameInput.vue' 49 import PlanFieldName from '../PlanFields/NameInput.vue'
83 import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' 50 import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
84 import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' 51 import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
85 import PlanFieldRadio from '../PlanFields/RadioGroup.vue' 52 import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
86 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' 53 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
54 +import { useFieldDependencies } from '@/composables/useFieldDependencies'
87 55
88 /** 56 /**
89 * 组件属性 57 * 组件属性
...@@ -105,6 +73,7 @@ const props = defineProps({ ...@@ -105,6 +73,7 @@ const props = defineProps({
105 * @property {Array<string>} payment_periods - 缴费年期选项 73 * @property {Array<string>} payment_periods - 缴费年期选项
106 * @property {Object} age_range - 年龄范围 { min, max } 74 * @property {Object} age_range - 年龄范围 { min, max }
107 * @property {string} insurance_period - 保险期间 75 * @property {string} insurance_period - 保险期间
76 + * @property {Object} form_schema - 表单 Schema
108 */ 77 */
109 config: { 78 config: {
110 type: Object, 79 type: Object,
...@@ -139,6 +108,104 @@ const form = reactive({}) ...@@ -139,6 +108,104 @@ const form = reactive({})
139 108
140 let previousModelValue = null 109 let previousModelValue = null
141 110
111 +// 字段类型与组件的对应关系
112 +const fieldComponentMap = {
113 + name: PlanFieldName,
114 + radio: PlanFieldRadio,
115 + date: PlanFieldDatePicker,
116 + amount: PlanFieldAmount,
117 + payment_period: PaymentPeriodRadio
118 +}
119 +
120 +// Schema 配置入口
121 +const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
122 +
123 +const fieldDefinitions = computed(() => {
124 + return baseFields.value.reduce((result, field) => {
125 + result[field.key] = field
126 + return result
127 + }, {})
128 +})
129 +
130 +/**
131 + * 获取字段对应的渲染组件
132 + * @param {Object} field - 字段配置
133 + * @returns {Object|null} Vue 组件
134 + */
135 +const getFieldComponent = (field) => {
136 + return fieldComponentMap[field.type] || null
137 +}
138 +
139 +/**
140 + * 组装字段渲染所需的 props
141 + * @param {Object} field - 字段配置
142 + * @returns {Object} 传入字段组件的 props
143 + */
144 +const getFieldProps = (field) => {
145 + const fieldProps = {
146 + label: field.label,
147 + placeholder: field.placeholder,
148 + required: !!field.required
149 + }
150 +
151 + if (field.options) {
152 + fieldProps.options = field.options
153 + }
154 +
155 + // 缴费年期选项由模板配置提供
156 + if (field.options_from === 'payment_periods') {
157 + fieldProps.options = fieldProps.options || props.config?.payment_periods
158 + }
159 +
160 + // 基础币种来自模板配置
161 + if (field.currency_from === 'currency') {
162 + fieldProps.currency = props.config?.currency
163 + }
164 +
165 + // 金额键盘的弹窗提示文本
166 + if (field.input_label) {
167 + fieldProps.inputLabel = field.input_label
168 + }
169 +
170 + return fieldProps
171 +}
172 +
173 +const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
174 +
175 +/**
176 + * 获取 Schema 默认值
177 + * @param {Object} value - 当前表单数据
178 + * @returns {Object} 默认值集合
179 + */
180 +const getSchemaDefaults = (value) => {
181 + const defaults = {}
182 + const fields = [...baseFields.value]
183 + fields.forEach(field => {
184 + if (field.default !== undefined && (value?.[field.key] === undefined || value?.[field.key] === null)) {
185 + defaults[field.key] = field.default
186 + }
187 + })
188 + return defaults
189 +}
190 +
191 +/**
192 + * 初始化表单数据
193 + * @param {Object} value - 初始数据
194 + */
195 +const initializeForm = (value) => {
196 + if (!value) {
197 + Object.keys(form).forEach(key => delete form[key])
198 + return
199 + }
200 +
201 + const defaults = getSchemaDefaults(value)
202 +
203 + Object.assign(form, {
204 + ...value,
205 + ...defaults
206 + })
207 +}
208 +
142 // 监听父组件的数据变化 209 // 监听父组件的数据变化
143 watch( 210 watch(
144 () => props.modelValue, 211 () => props.modelValue,
...@@ -157,58 +224,120 @@ watch( ...@@ -157,58 +224,120 @@ watch(
157 224
158 if (isReset) { 225 if (isReset) {
159 // 父组件重置了:清空表单 226 // 父组件重置了:清空表单
160 - Object.keys(form).forEach(key => delete form[key]) 227 + initializeForm(newVal)
161 previousModelValue = newVal 228 previousModelValue = newVal
162 } else { 229 } else {
163 - // 正常更新:合并新字段,不删除已有字段 230 + // 正常更新:合并新字段,保留默认值逻辑
164 - // 这很重要!因为用户可能刚填写了某些字段,其他字段还没更新 231 + const defaults = getSchemaDefaults(newVal)
165 - Object.keys(newVal).forEach(key => { 232 + Object.assign(form, {
166 - form[key] = newVal[key] 233 + ...newVal,
234 + ...defaults
167 }) 235 })
168 previousModelValue = newVal 236 previousModelValue = newVal
169 } 237 }
170 }, 238 },
171 - { immediate: true } 239 + { immediate: true, deep: true }
172 ) 240 )
173 241
174 /** 242 /**
175 * 监听表单数据变化,同步到父组件 243 * 监听表单数据变化,同步到父组件
176 */ 244 */
245 +// 监听表单数据变化,同步到父组件
177 watch( 246 watch(
178 - () => form, 247 + form,
179 - (newVal) => emit('update:modelValue', newVal), 248 + (newVal) => emit('update:modelValue', { ...newVal }),
180 { deep: true } 249 { deep: true }
181 ) 250 )
182 251
183 /** 252 /**
184 - * 表单校验 253 + * 百分比输入清洗,避免非法字符
185 - * @returns {boolean} 是否通过校验 254 + * @param {string|number} value - 输入值
255 + * @param {string} key - 目标字段 key
186 */ 256 */
187 -const validate = () => { 257 +const onPercentageInput = (value, key) => {
188 - if (!form.customer_name || !form.customer_name.trim()) { 258 + // 转换为字符串(处理 value 为 null 或其他类型的情况)
189 - Taro.showToast({ title: '请输入申请人', icon: 'none' }) 259 + let strValue = String(value ?? '')
190 - return false 260 +
261 + // 只保留数字和小数点
262 + let cleaned = strValue.replace(/[^\d.]/g, '')
263 +
264 + // 只保留一个小数点
265 + const parts = cleaned.split('.')
266 + if (parts.length > 2) {
267 + cleaned = parts[0] + '.' + parts.slice(1).join('')
191 } 268 }
192 - if (!form.gender) { 269 +
193 - Taro.showToast({ title: '请选择性别', icon: 'none' }) 270 + // 限制小数点后最多 2 位
194 - return false 271 + if (parts.length === 2 && parts[1].length > 2) {
272 + cleaned = parts[0] + '.' + parts[1].slice(0, 2)
273 + }
274 +
275 + // 限制范围:0-100
276 + const numValue = parseFloat(cleaned)
277 + if (!Number.isNaN(numValue)) {
278 + if (numValue > 100) {
279 + cleaned = '100'
280 + } else if (numValue < 0) {
281 + cleaned = '0'
195 } 282 }
196 - if (!form.birthday) {
197 - Taro.showToast({ title: '请选择出生年月日', icon: 'none' })
198 - return false
199 } 283 }
200 - if (!form.smoker) { 284 +
201 - Taro.showToast({ title: '请选择是否吸烟', icon: 'none' }) 285 + form[key] = cleaned
286 +}
287 +
288 +const isEmptyValue = (value) => {
289 + if (value === null || value === undefined) return true
290 + if (typeof value === 'string' && value.trim() === '') return true
291 + if (Array.isArray(value) && value.length === 0) return true
202 return false 292 return false
293 +}
294 +
295 +const getRequiredMessage = (field) => {
296 + if (field?.placeholder) return field.placeholder
297 + const label = field?.label || '必填信息'
298 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
299 + if (selectTypes.includes(field?.type)) {
300 + return `请选择${label}`
203 } 301 }
204 - if (!form.coverage) { 302 + return `请输入${label}`
205 - Taro.showToast({ title: '请输入保额', icon: 'none' }) 303 +}
304 +
305 +const isFieldRequired = (field) => {
306 + return field?.required === true || field?.required === undefined
307 +}
308 +
309 +/**
310 + * 表单校验(基于 Schema)
311 + * @returns {boolean} 校验是否通过
312 + */
313 +const validate = () => {
314 + const fields = [...baseFields.value]
315 +
316 + for (const field of fields) {
317 + if (!isFieldVisible(field.key)) {
318 + continue
319 + }
320 +
321 + if (isFieldRequired(field)) {
322 + const value = form[field.key]
323 + if (isEmptyValue(value)) {
324 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
206 return false 325 return false
207 } 326 }
208 - if (!form.payment_period) { 327 + }
209 - Taro.showToast({ title: '请选择缴费年期', icon: 'none' }) 328 +
329 + if (field.type === 'percentage' && isFieldVisible(field.key)) {
330 + const value = form[field.key]
331 + if (!isEmptyValue(value)) {
332 + const percentage = parseFloat(value)
333 + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
334 + Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
210 return false 335 return false
211 } 336 }
337 + }
338 + }
339 + }
340 +
212 return true 341 return true
213 } 342 }
214 343
......
1 <template> 1 <template>
2 <div v-if="config"> 2 <div v-if="config">
3 - <!-- 申请人 --> 3 + <template v-for="field in baseFields" :key="field.id || field.key">
4 - <PlanFieldName 4 + <component
5 - v-model="form.customer_name" 5 + v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
6 - label="申请人" 6 + :is="getFieldComponent(field)"
7 - placeholder="请输入申请人" 7 + v-model="form[field.key]"
8 - :required="true" 8 + v-bind="getFieldProps(field)"
9 class="mb-5" 9 class="mb-5"
10 /> 10 />
11 - 11 + <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
12 - <!-- 性别 --> 12 + <div class="text-sm text-gray-700 mb-2 flex items-center">
13 - <PlanFieldRadio 13 + <span v-if="field.required" class="text-red-500 mr-1">*</span>
14 - v-model="form.gender" 14 + <span>{{ field.label }}</span>
15 - label="性别" 15 + </div>
16 - :options="['男', '女']" 16 + <nut-input
17 - :required="true" 17 + v-model="form[field.key]"
18 - class="mb-5" 18 + type="digit"
19 - /> 19 + :placeholder="field.placeholder"
20 - 20 + @input="(value) => onPercentageInput(value, field.key)"
21 - <!-- 出生日期 --> 21 + class="w-full"
22 - <PlanFieldDatePicker
23 - v-model="form.birthday"
24 - label="出生年月日"
25 - placeholder="请选择年月日"
26 - :required="true"
27 - class="mb-5"
28 - />
29 -
30 - <!-- 是否吸烟 -->
31 - <PlanFieldRadio
32 - v-model="form.smoker"
33 - label="是否吸烟"
34 - :options="['是', '否']"
35 - :required="true"
36 - class="mb-5"
37 - />
38 -
39 - <!-- 保额(年缴保费) -->
40 - <PlanFieldAmount
41 - v-model="form.coverage"
42 - label="年缴保费"
43 - placeholder="请输入年缴保费"
44 - :input-label="'请输入年缴保费金额'"
45 - :currency="config.currency"
46 - :required="true"
47 - class="mb-5"
48 - />
49 -
50 - <!-- 缴费年期 - 单选形式 -->
51 - <PaymentPeriodRadio
52 - v-model="form.payment_period"
53 - label="缴费年期"
54 - :options="config.payment_periods"
55 - :required="true"
56 - class="mb-5"
57 /> 22 />
23 + </div>
24 + </template>
58 25
59 - <!-- 分割线 -->
60 <div class="border-t border-gray-200 my-6"></div> 26 <div class="border-t border-gray-200 my-6"></div>
61 27
62 - <!-- 提取计划配置 -->
63 <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section"> 28 <div v-if="config.withdrawal_plan?.enabled" class="withdrawal-plan-section">
64 - <!-- 第一层:是否希望生成一份允许减少名义金额的提取说明? --> 29 + <template v-for="field in withdrawalFields" :key="field.id || field.key">
65 - <PlanFieldRadio 30 + <h3 v-if="field.section_title" class="text-base font-semibold text-gray-900 mb-4">
66 - v-model="form.withdrawal_enabled" 31 + {{ field.section_title }}
67 - label="是否希望生成一份允许减少名义金额的提取说明?" 32 + </h3>
68 - :options="['是', '否']" 33 + <component
69 - :required="true" 34 + v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
70 - class="mb-5" 35 + :is="getFieldComponent(field)"
71 - /> 36 + v-model="form[field.key]"
72 - 37 + v-bind="getFieldProps(field)"
73 - <!-- 仅当选择"是"时才显示以下内容 -->
74 - <template v-if="form.withdrawal_enabled === '是'">
75 - <h3 class="text-base font-semibold text-gray-900 mb-4">款项提取(允许减少名义金额)</h3>
76 -
77 - <!-- 提取选项:指定提取金额 / 最高固定提取金额 -->
78 - <PlanFieldRadio
79 - v-model="form.withdrawal_mode"
80 - label="提取选项"
81 - :options="['指定提取金额', '最高固定提取金额']"
82 - :required="true"
83 - @change="onWithdrawalModeChange"
84 - class="mb-5"
85 - />
86 -
87 - <!-- 指定提取金额模式 -->
88 - <template v-if="form.withdrawal_mode === '指定提取金额'">
89 - <!-- 提取方式:只有按年岁 -->
90 - <PlanFieldRadio
91 - v-model="form.specified_amount_type"
92 - label="提取方式"
93 - :options="['按年岁']"
94 - :required="true"
95 - class="mb-5"
96 - />
97 -
98 - <!-- 按年岁 -->
99 - <template v-if="form.specified_amount_type === '按年岁'">
100 - <!-- 每年提取金额 -->
101 - <PlanFieldAmount
102 - v-model="form.annual_withdrawal_amount"
103 - label="每年提取金额"
104 - placeholder="请输入每年提取金额"
105 - :input-label="'请输入每年提取金额'"
106 - :currency="config.withdrawal_plan.default_currency"
107 - :required="true"
108 - class="mb-5"
109 - />
110 -
111 - <!-- 由几岁开始 -->
112 - <PlanFieldAgePicker
113 - v-model="form.withdrawal_start_age"
114 - label="由几岁开始"
115 - placeholder="请输入开始提取年龄"
116 - :required="true"
117 class="mb-5" 38 class="mb-5"
118 /> 39 />
119 - 40 + <div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
120 - <!-- 提取期(年) -->
121 - <PlanFieldSelect
122 - v-model="form.withdrawal_period"
123 - label="提取期(年)"
124 - placeholder="请选择提取期"
125 - :options="withdrawalPeriods"
126 - :required="true"
127 - class="mb-5"
128 - />
129 -
130 - <!-- 每年递增提取之百分比 -->
131 - <div class="mb-5">
132 <div class="text-sm text-gray-700 mb-2 flex items-center"> 41 <div class="text-sm text-gray-700 mb-2 flex items-center">
133 - <span class="text-red-500 mr-1">*</span> 42 + <span v-if="field.required" class="text-red-500 mr-1">*</span>
134 - <span>每年递增提取之百分比(%</span> 43 + <span>{{ field.label }}</span>
135 </div> 44 </div>
136 <nut-input 45 <nut-input
137 - v-model="form.annual_increase_percentage" 46 + v-model="form[field.key]"
138 type="digit" 47 type="digit"
139 - placeholder="请输入递增百分比" 48 + :placeholder="field.placeholder"
140 - @input="onPercentageInput" 49 + @input="(value) => onPercentageInput(value, field.key)"
141 class="w-full" 50 class="w-full"
142 /> 51 />
143 </div> 52 </div>
144 </template> 53 </template>
145 - </template>
146 -
147 - <!-- 最高固定提取金额模式 -->
148 - <template v-if="form.withdrawal_mode === '最高固定提取金额'">
149 - <!-- 按年岁:由几岁开始 -->
150 - <PlanFieldAgePicker
151 - v-model="form.withdrawal_start_age"
152 - label="按年岁:由几岁开始"
153 - placeholder="请输入开始提取年龄"
154 - :required="true"
155 - class="mb-5"
156 - />
157 -
158 - <!-- 提取期(年) -->
159 - <PlanFieldSelect
160 - v-model="form.withdrawal_period"
161 - label="提取期(年)"
162 - placeholder="请选择提取期"
163 - :options="withdrawalPeriods"
164 - :required="true"
165 - class="mb-5"
166 - />
167 - </template>
168 - </template>
169 </div> 54 </div>
170 </div> 55 </div>
171 56
...@@ -183,7 +68,6 @@ ...@@ -183,7 +68,6 @@
183 * @description GS/GC/FA/LV2 等储蓄型保险产品的计划书录入表单 68 * @description GS/GC/FA/LV2 等储蓄型保险产品的计划书录入表单
184 * - 表单字段:性别、出生年月日、是否吸烟、保额、缴费年期 69 * - 表单字段:性别、出生年月日、是否吸烟、保额、缴费年期
185 * - 提取计划:指定提取金额(按年岁/按保单年度)、最高固定提取金额 70 * - 提取计划:指定提取金额(按年岁/按保单年度)、最高固定提取金额
186 - * - 小程序端币种固定(使用配置中的默认币种)
187 * @author Claude Code 71 * @author Claude Code
188 * @example 72 * @example
189 * <SavingsTemplate 73 * <SavingsTemplate
...@@ -200,6 +84,7 @@ import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' ...@@ -200,6 +84,7 @@ import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
200 import PlanFieldRadio from '../PlanFields/RadioGroup.vue' 84 import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
201 import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' 85 import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
202 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' 86 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
87 +import { useFieldDependencies } from '@/composables/useFieldDependencies'
203 88
204 /** 89 /**
205 * 组件属性 90 * 组件属性
...@@ -261,22 +146,121 @@ const form = reactive({}) ...@@ -261,22 +146,121 @@ const form = reactive({})
261 146
262 let previousModelValue = null 147 let previousModelValue = null
263 148
264 -// 初始化默认值 149 +// 字段类型与组件的对应关系
150 +const fieldComponentMap = {
151 + name: PlanFieldName,
152 + radio: PlanFieldRadio,
153 + date: PlanFieldDatePicker,
154 + amount: PlanFieldAmount,
155 + age: PlanFieldAgePicker,
156 + select: PlanFieldSelect,
157 + payment_period: PaymentPeriodRadio
158 +}
159 +
160 +// Schema 配置入口
161 +const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
162 +const withdrawalFields = computed(() => props.config?.form_schema?.withdrawal_fields || [])
163 +const resetMap = computed(() => props.config?.form_schema?.reset_map || {})
164 +
165 +const fieldDefinitions = computed(() => {
166 + return [...baseFields.value, ...withdrawalFields.value].reduce((result, field) => {
167 + result[field.key] = field
168 + return result
169 + }, {})
170 +})
171 +
172 +/**
173 + * 获取字段对应的渲染组件
174 + * @param {Object} field - 字段配置
175 + * @returns {Object|null} Vue 组件
176 + */
177 +const getFieldComponent = (field) => {
178 + return fieldComponentMap[field.type] || null
179 +}
180 +
181 +/**
182 + * 组装字段渲染所需的 props
183 + * @param {Object} field - 字段配置
184 + * @returns {Object} 传入字段组件的 props
185 + */
186 +const getFieldProps = (field) => {
187 + const fieldProps = {
188 + label: field.label,
189 + placeholder: field.placeholder,
190 + required: !!field.required
191 + }
192 +
193 + if (field.options) {
194 + fieldProps.options = field.options
195 + }
196 +
197 + // 缴费年期选项由模板配置提供
198 + if (field.options_from === 'payment_periods') {
199 + fieldProps.options = fieldProps.options || props.config?.payment_periods
200 + }
201 +
202 + // 提取期选项由提取计划配置提供
203 + if (field.options_from === 'withdrawal_plan.withdrawal_periods') {
204 + fieldProps.options = fieldProps.options || props.config?.withdrawal_plan?.withdrawal_periods
205 + }
206 +
207 + // 基础币种来自模板配置
208 + if (field.currency_from === 'currency') {
209 + fieldProps.currency = props.config?.currency
210 + }
211 +
212 + // 提取计划币种来自提取计划配置
213 + if (field.currency_from === 'withdrawal_plan.default_currency') {
214 + fieldProps.currency = props.config?.withdrawal_plan?.default_currency
215 + }
216 +
217 + // 金额键盘的弹窗提示文本
218 + if (field.input_label) {
219 + fieldProps.inputLabel = field.input_label
220 + }
221 +
222 + return fieldProps
223 +}
224 +
225 +const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
226 +
227 +/**
228 + * 获取 Schema 默认值
229 + * @param {Object} value - 当前表单数据
230 + * @returns {Object} 默认值集合
231 + */
232 +const getSchemaDefaults = (value) => {
233 + const defaults = {}
234 + const fields = [...baseFields.value, ...withdrawalFields.value]
235 + fields.forEach(field => {
236 + if (field.default !== undefined && (value?.[field.key] === undefined || value?.[field.key] === null)) {
237 + defaults[field.key] = field.default
238 + }
239 + })
240 + return defaults
241 +}
242 +
243 +/**
244 + * 初始化表单数据
245 + * @param {Object} value - 初始数据
246 + */
265 const initializeForm = (value) => { 247 const initializeForm = (value) => {
266 if (!value) { 248 if (!value) {
267 Object.keys(form).forEach(key => delete form[key]) 249 Object.keys(form).forEach(key => delete form[key])
268 return 250 return
269 } 251 }
270 252
253 + const defaults = getSchemaDefaults(value)
254 +
271 Object.assign(form, { 255 Object.assign(form, {
272 ...value, 256 ...value,
273 - // 默认值 257 + ...defaults,
274 - withdrawal_enabled: value.withdrawal_enabled || '否',
275 - withdrawal_mode: value.withdrawal_mode || '指定提取金额',
276 - specified_amount_type: value.specified_amount_type || '按年岁',
277 - // 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
278 annual_withdrawal_amount: value.annual_withdrawal_amount ?? null, 258 annual_withdrawal_amount: value.annual_withdrawal_amount ?? null,
279 - annual_increase_percentage: value.annual_increase_percentage ?? null 259 + annual_increase_percentage: value.annual_increase_percentage ?? null,
260 + withdrawal_start_age_specified: value.withdrawal_start_age_specified ?? null,
261 + withdrawal_period_specified: value.withdrawal_period_specified ?? null,
262 + withdrawal_start_age_fixed: value.withdrawal_start_age_fixed ?? null,
263 + withdrawal_period_fixed: value.withdrawal_period_fixed ?? null
280 }) 264 })
281 } 265 }
282 266
...@@ -302,15 +286,16 @@ watch( ...@@ -302,15 +286,16 @@ watch(
302 previousModelValue = newVal 286 previousModelValue = newVal
303 } else { 287 } else {
304 // 正常更新:合并新字段,保留默认值逻辑 288 // 正常更新:合并新字段,保留默认值逻辑
289 + const defaults = getSchemaDefaults(newVal)
305 Object.assign(form, { 290 Object.assign(form, {
306 ...newVal, 291 ...newVal,
307 - // 默认值 292 + ...defaults,
308 - withdrawal_enabled: newVal.withdrawal_enabled || '否',
309 - withdrawal_mode: newVal.withdrawal_mode || '指定提取金额',
310 - specified_amount_type: newVal.specified_amount_type || '按年岁',
311 - // 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
312 annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null, 293 annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null,
313 - annual_increase_percentage: newVal.annual_increase_percentage ?? null 294 + annual_increase_percentage: newVal.annual_increase_percentage ?? null,
295 + withdrawal_start_age_specified: newVal.withdrawal_start_age_specified ?? null,
296 + withdrawal_period_specified: newVal.withdrawal_period_specified ?? null,
297 + withdrawal_start_age_fixed: newVal.withdrawal_start_age_fixed ?? null,
298 + withdrawal_period_fixed: newVal.withdrawal_period_fixed ?? null
314 }) 299 })
315 previousModelValue = newVal 300 previousModelValue = newVal
316 } 301 }
...@@ -321,74 +306,47 @@ watch( ...@@ -321,74 +306,47 @@ watch(
321 /** 306 /**
322 * 监听表单数据变化,同步到父组件 307 * 监听表单数据变化,同步到父组件
323 */ 308 */
309 +// 监听提取模式切换,按配置清空脏数据
324 watch( 310 watch(
325 () => form, 311 () => form,
326 - (newVal) => emit('update:modelValue', newVal), 312 + (newVal) => {
313 + emit('update:modelValue', newVal)
314 + },
327 { deep: true } 315 { deep: true }
328 ) 316 )
329 317
330 /** 318 /**
331 - * 提取年期选项(从配置读取) 319 + * 监听提取模式变化,清空对应字段
332 - * @type {ComputedRef<Array<string>>}
333 - */
334 -const withdrawalPeriods = computed(() => {
335 - return props.config?.withdrawal_plan?.withdrawal_periods || [
336 - '1年',
337 - '2年',
338 - '3年',
339 - '5年',
340 - '10年',
341 - '15年',
342 - '20年',
343 - '终身'
344 - ]
345 -})
346 -
347 -/**
348 - * 提取模式变化时的处理
349 - * @param {string} mode - 新的提取模式
350 - *
351 - * @description 当用户切换提取模式时,清除不相关的字段
352 - * - 切换到"指定提取金额":保留字段,等待用户选择子选项
353 - * - 切换到"最高固定金额":清除指定金额相关字段
354 - */
355 -const onWithdrawalModeChange = (mode) => {
356 - if (mode === '最高固定提取金额') {
357 - // 最高固定金额模式不需要指定金额的相关字段
358 - delete form.specified_amount_type
359 - delete form.annual_withdrawal_amount
360 - delete form.annual_increase_percentage
361 - } else if (mode === '指定提取金额') {
362 - // 指定提取金额模式(按年岁),保留现有字段
363 - }
364 -}
365 -
366 -/**
367 - * 监听提取计划启用状态变化
368 - * @description 当用户选择"否"时,清除所有提取计划相关字段
369 */ 320 */
370 watch( 321 watch(
371 - () => form.withdrawal_enabled, 322 + () => form.withdrawal_mode,
372 - (newValue) => { 323 + (newMode) => {
373 - if (newValue === '否') { 324 + const resetFields = resetMap.value?.withdrawal_mode?.[newMode] || []
374 - // 清除所有提取计划相关字段 325 + if (resetFields.length > 0) {
375 - delete form.withdrawal_mode 326 + resetFields.forEach(key => {
376 - delete form.specified_amount_type 327 + form[key] = null
377 - delete form.withdrawal_start_age 328 + })
378 - delete form.withdrawal_period 329 + emit('update:modelValue', { ...form })
379 - delete form.annual_withdrawal_amount
380 - delete form.annual_increase_percentage
381 } 330 }
382 } 331 }
383 ) 332 )
384 333
385 /** 334 /**
335 + * 提取年期选项(从配置读取)
336 + * @type {ComputedRef<Array<string>>}
337 + */
338 +/**
386 * 百分比输入限制(实时) 339 * 百分比输入限制(实时)
387 * @description 限制百分比输入为有效数值,最多2位小数 340 * @description 限制百分比输入为有效数值,最多2位小数
388 * 只允许输入数字和一个小数点 341 * 只允许输入数字和一个小数点
389 * @param {string} value - 输入值 342 * @param {string} value - 输入值
390 */ 343 */
391 -const onPercentageInput = (value) => { 344 +/**
345 + * 百分比输入清洗,避免非法字符
346 + * @param {string|number} value - 输入值
347 + * @param {string} key - 目标字段 key
348 + */
349 +const onPercentageInput = (value, key) => {
392 // 转换为字符串(处理 value 为 null 或其他类型的情况) 350 // 转换为字符串(处理 value 为 null 或其他类型的情况)
393 let strValue = String(value ?? '') 351 let strValue = String(value ?? '')
394 352
...@@ -416,97 +374,63 @@ const onPercentageInput = (value) => { ...@@ -416,97 +374,63 @@ const onPercentageInput = (value) => {
416 } 374 }
417 } 375 }
418 376
419 - // 更新表单值 377 + form[key] = cleaned
420 - form.annual_increase_percentage = cleaned
421 } 378 }
422 379
423 -/** 380 +const isEmptyValue = (value) => {
424 - * 表单校验 381 + if (value === null || value === undefined) return true
425 - * @returns {boolean} 是否通过校验 382 + if (typeof value === 'string' && value.trim() === '') return true
426 - */ 383 + if (Array.isArray(value) && value.length === 0) return true
427 -const validate = () => {
428 - // 基础字段校验
429 - if (!form.customer_name || !form.customer_name.trim()) {
430 - Taro.showToast({ title: '请输入申请人', icon: 'none' })
431 - return false
432 - }
433 - if (!form.gender) {
434 - Taro.showToast({ title: '请选择性别', icon: 'none' })
435 - return false
436 - }
437 - if (!form.birthday) {
438 - Taro.showToast({ title: '请选择出生年月日', icon: 'none' })
439 - return false
440 - }
441 - if (!form.smoker) {
442 - Taro.showToast({ title: '请选择是否吸烟', icon: 'none' })
443 - return false
444 - }
445 - if (!form.coverage) {
446 - Taro.showToast({ title: '请输入年缴保费', icon: 'none' })
447 - return false
448 - }
449 - if (!form.payment_period) {
450 - Taro.showToast({ title: '请选择缴费年期', icon: 'none' })
451 - return false
452 - }
453 -
454 - // 提取计划校验
455 - if (props.config.withdrawal_plan?.enabled) {
456 - if (!form.withdrawal_enabled) {
457 - Taro.showToast({ title: '请选择是否希望生成提取说明', icon: 'none' })
458 return false 384 return false
459 - } 385 +}
460 386
461 - if (form.withdrawal_enabled === '是') { 387 +const getRequiredMessage = (field) => {
462 - if (!form.withdrawal_mode) { 388 + if (field?.placeholder) return field.placeholder
463 - Taro.showToast({ title: '请选择提取选项', icon: 'none' }) 389 + const label = field?.label || '必填信息'
464 - return false 390 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
391 + if (selectTypes.includes(field?.type)) {
392 + return `请选择${label}`
465 } 393 }
394 + return `请输入${label}`
395 +}
466 396
467 - if (form.withdrawal_mode === '指定提取金额') { 397 +const isFieldRequired = (field) => {
468 - if (!form.specified_amount_type) { 398 + return field?.required === true || field?.required === undefined
469 - Taro.showToast({ title: '请选择提取方式', icon: 'none' }) 399 +}
470 - return false
471 - }
472 400
473 - if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') { 401 +/**
474 - Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' }) 402 + * 表单校验
475 - return false 403 + * @returns {boolean} 是否通过校验
476 - } 404 + */
405 +/**
406 + * 表单校验(基于 Schema)
407 + * @returns {boolean} 校验是否通过
408 + */
409 +const validate = () => {
410 + const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])]
477 411
478 - if (!form.withdrawal_period) { 412 + for (const field of fields) {
479 - Taro.showToast({ title: '请选择提取期', icon: 'none' }) 413 + if (!isFieldVisible(field.key)) {
480 - return false 414 + continue
481 } 415 }
482 416
483 - if (form.specified_amount_type === '按年岁') { 417 + if (isFieldRequired(field)) {
484 - if (!form.annual_withdrawal_amount || form.annual_withdrawal_amount === '') { 418 + const value = form[field.key]
485 - Taro.showToast({ title: '请输入每年提取金额', icon: 'none' }) 419 + if (isEmptyValue(value)) {
420 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
486 return false 421 return false
487 } 422 }
488 - if (form.annual_increase_percentage === undefined || form.annual_increase_percentage === '') {
489 - Taro.showToast({ title: '请输入每年递增提取之百分比', icon: 'none' })
490 - return false
491 } 423 }
492 424
493 - // 验证百分比范围 425 + if (field.type === 'percentage' && isFieldVisible(field.key)) {
494 - const percentage = parseFloat(form.annual_increase_percentage) 426 + const value = form[field.key]
427 + if (!isEmptyValue(value)) {
428 + const percentage = parseFloat(value)
495 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { 429 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
496 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) 430 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
497 return false 431 return false
498 } 432 }
499 } 433 }
500 - } else if (form.withdrawal_mode === '最高固定提取金额') {
501 - if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') {
502 - Taro.showToast({ title: '请输入开始提取年龄', icon: 'none' })
503 - return false
504 - }
505 - if (!form.withdrawal_period) {
506 - Taro.showToast({ title: '请选择提取期', icon: 'none' })
507 - return false
508 - }
509 - }
510 } 434 }
511 } 435 }
512 436
......
1 +/**
2 + * useFieldDependencies 单元测试
3 + *
4 + * @description 测试字段关联系统的显示/隐藏逻辑
5 + * @module composables/__tests__/useFieldDependencies.test
6 + */
7 +
8 +import { describe, it, expect, beforeEach, vi } from 'vitest'
9 +import { reactive } from 'vue'
10 +import { useFieldDependencies } from '../useFieldDependencies'
11 +import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
12 +
13 +describe('useFieldDependencies', () => {
14 + let formData, deps
15 +
16 + beforeEach(() => {
17 + formData = reactive({
18 + withdrawal_enabled: false,
19 + withdrawal_mode: '',
20 + withdrawal_start_age: null
21 + })
22 + deps = useFieldDependencies(formData)
23 + })
24 +
25 + it('should initialize field states', () => {
26 + expect(deps.fieldVisibility.withdrawal_enabled).toBe(true)
27 + expect(deps.fieldVisibility.withdrawal_mode).toBe(false) // 受影响,默认隐藏
28 + })
29 +
30 + it('should hide fields when dependency is false', () => {
31 + // withdrawal_enabled = false,withdrawal_mode 应该隐藏
32 + expect(deps.isFieldVisible('withdrawal_mode')).toBe(false)
33 + expect(deps.isFieldEnabled('withdrawal_mode')).toBe(false)
34 + })
35 +
36 + it('should show fields when dependency becomes true', () => {
37 + // 启用提取
38 + deps.updateFieldValue('withdrawal_enabled', true)
39 +
40 + // withdrawal_mode 应该显示
41 + expect(deps.isFieldVisible('withdrawal_mode')).toBe(true)
42 + expect(deps.isFieldEnabled('withdrawal_mode')).toBe(true)
43 + expect(deps.fieldVisibility.withdrawal_mode).toBe(true)
44 + })
45 +
46 + it('should update affected fields when dependency changes', () => {
47 + // 初始状态
48 + expect(deps.fieldVisibility.withdrawal_mode).toBe(false)
49 +
50 + // 启用提取
51 + deps.updateFieldValue('withdrawal_enabled', true)
52 +
53 + // 检查状态已更新
54 + expect(deps.fieldVisibility.withdrawal_mode).toBe(true)
55 +
56 + // 禁用提取
57 + deps.updateFieldValue('withdrawal_enabled', false)
58 +
59 + // 状态应该隐藏
60 + expect(deps.fieldVisibility.withdrawal_mode).toBe(false)
61 + })
62 +
63 + it('should handle show_when conditions', () => {
64 + // 测试 show_when 条件
65 + const definition = PLAN_FIELD_DEFINITIONS.withdrawal_mode
66 + expect(definition.show_when).toEqual({ withdrawal_enabled: true })
67 +
68 + // 当条件不满足时
69 + expect(deps.isFieldVisible('withdrawal_mode')).toBe(false)
70 +
71 + // 满足条件
72 + deps.updateFieldValue('withdrawal_enabled', true)
73 + expect(deps.isFieldVisible('withdrawal_mode')).toBe(true)
74 + })
75 +
76 + it('should return list of visible fields', () => {
77 + // 初始状态(withdrawal_enabled = false)
78 + expect(deps.visibleFields.value).toContain('withdrawal_enabled')
79 + expect(deps.visibleFields.value).not.toContain('withdrawal_mode')
80 +
81 + // 启用后
82 + deps.updateFieldValue('withdrawal_enabled', true)
83 + expect(deps.visibleFields.value).toContain('withdrawal_mode')
84 + })
85 +
86 + it('should handle multiple affected fields', () => {
87 + // withdrawal_enabled affects multiple fields
88 + const affectedFields = PLAN_FIELD_DEFINITIONS.withdrawal_enabled.affects
89 + expect(affectedFields.length).toBeGreaterThan(0)
90 +
91 + // 启用后,所有受影响字段应该可见
92 + deps.updateFieldValue('withdrawal_enabled', true)
93 +
94 + for (const field of affectedFields) {
95 + expect(deps.isFieldVisible(field)).toBe(true)
96 + }
97 + })
98 +
99 + it('should handle fields without dependencies', () => {
100 + // customer_name 没有依赖,应该始终显示
101 + expect(deps.isFieldVisible('customer_name')).toBe(true)
102 + expect(deps.isFieldEnabled('customer_name')).toBe(true)
103 + })
104 +
105 + it('should detect circular dependencies in development', () => {
106 + const originalEnv = process.env.NODE_ENV
107 + process.env.NODE_ENV = 'development'
108 + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
109 +
110 + PLAN_FIELD_DEFINITIONS.circular_a = {
111 + affects: ['circular_b']
112 + }
113 + PLAN_FIELD_DEFINITIONS.circular_b = {
114 + affects: ['circular_a']
115 + }
116 +
117 + const localFormData = reactive({})
118 + useFieldDependencies(localFormData)
119 +
120 + expect(consoleSpy).toHaveBeenCalled()
121 +
122 + delete PLAN_FIELD_DEFINITIONS.circular_a
123 + delete PLAN_FIELD_DEFINITIONS.circular_b
124 + consoleSpy.mockRestore()
125 + process.env.NODE_ENV = originalEnv
126 + })
127 +})
1 +/**
2 + * useFieldValueTransform 单元测试
3 + *
4 + * @description 测试字段值转换 Composable
5 + * @module composables/__tests__/useFieldValueTransform.test
6 + */
7 +
8 +import { ref } from 'vue'
9 +import { describe, it, expect, beforeEach } from 'vitest'
10 +import { useFieldValueTransform } from '../useFieldValueTransform'
11 +import { PLAN_FIELD_DEFINITIONS, TRANSFORM_TYPES } from '@/config/plan-fields'
12 +
13 +describe('useFieldValueTransform', () => {
14 + describe('toYuan - 分转元(用于显示)', () => {
15 + it('should convert fen value to yuan format', () => {
16 + const formData = ref({ coverage: '1000000' }) // 分值整数(10000元×100)
17 + const fieldDefinitions = PLAN_FIELD_DEFINITIONS
18 +
19 + const { toYuan } = useFieldValueTransform(formData, fieldDefinitions)
20 +
21 + // 分值 1000000(10000元×100)转为元值:÷100 = 10000.00(保留两位小数)
22 + expect(toYuan('coverage', '1000000')).toBe('10000.00')
23 + expect(toYuan('coverage', '1500000')).toBe('15000.00')
24 + })
25 +
26 + it('should convert yuan decimal string correctly', () => {
27 + const formData = ref({ coverage: '1000050' }) // 分值整数
28 + const { toYuan } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
29 +
30 + expect(toYuan('coverage', '1000050')).toBe('10000.50') // 分转元:÷100,保留两位小数
31 + })
32 +
33 + it('should return yuan value directly for fields without transform', () => {
34 + const formData = ref({ customer_name: '张三' })
35 + const { toYuan } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
36 +
37 + expect(toYuan('customer_name', '张三')).toBe('张三')
38 + })
39 +
40 + it('should handle null values', () => {
41 + const formData = ref({ coverage: null })
42 + const { toYuan } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
43 +
44 + expect(toYuan('coverage', null)).toBe(null)
45 + })
46 +
47 + it('should handle undefined values', () => {
48 + const formData = ref({ coverage: undefined })
49 + const { toYuan } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
50 +
51 + expect(toYuan('coverage', undefined)).toBe(undefined)
52 + })
53 +
54 + it('should return string for fen values (keep 2 decimal places)', () => {
55 + const formData = ref({ coverage: '100005' }) // 分值字符串(10000.05元×100)
56 + const { toYuan } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
57 +
58 + // fenToYuan 返回字符串格式的元值
59 + expect(toYuan('coverage', '100005')).toBe('1000.05') // 分→元:÷100,保留两位小数
60 + })
61 + })
62 +
63 + describe('toFen - 元转分(用于提交)', () => {
64 + it('should convert yuan value to fen', () => {
65 + const formData = ref({ coverage: '10000' }) // 元值
66 + const { toFen } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
67 +
68 + expect(toFen('coverage', '10000')).toBe(1000000) // 元→分:×100
69 + })
70 +
71 + it('should convert yuan string to fen', () => {
72 + const formData = ref({ coverage: '10000.00' }) // 元值字符串
73 + const { toFen } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
74 +
75 + expect(toFen('coverage', '10000.00')).toBe(1000000) // 元→分:×100
76 + })
77 +
78 + it('should handle null values', () => {
79 + const formData = ref({ coverage: null })
80 + const { toFen } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
81 +
82 + expect(toFen('coverage', null)).toBe(null)
83 + })
84 +
85 + it('should handle undefined values', () => {
86 + const formData = ref({ coverage: undefined })
87 + const { toFen } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
88 +
89 + expect(toFen('coverage', undefined)).toBe(undefined)
90 + })
91 +
92 + it('should return original value for fields without transform', () => {
93 + const formData = ref({ customer_name: '张三' })
94 + const { toFen } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
95 +
96 + expect(toFen('customer_name', '张三')).toBe('张三')
97 + })
98 + })
99 +
100 + describe('batchToFen - 批量元转分', () => {
101 + it('should convert all yuan fields to fen', () => {
102 + const formData = ref({
103 + coverage: 10000, // 元值→分值
104 + withdrawal_period: 3,
105 + customer_name: '张三'
106 + })
107 + const { submitData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
108 +
109 + const result = submitData.value
110 + expect(result.coverage).toBe(1000000) // 元转分:×100
111 + expect(result.withdrawal_period).toBe(3)
112 + expect(result.customer_name).toBe('张三')
113 + })
114 +
115 + it('should skip fields without transform attribute', () => {
116 + const formData = ref({
117 + customer_name: '张三',
118 + gender: 'male'
119 + })
120 + const { submitData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
121 +
122 + const result = submitData.value
123 + expect(result.customer_name).toBe('张三')
124 + expect(result.gender).toBe('male')
125 + })
126 + })
127 +
128 + describe('batchToFen - 批量元转分(用于提交)', () => {
129 + it('should convert all yuan fields to fen', () => {
130 + const formData = ref({
131 + coverage: '10000', // 元值→分值
132 + withdrawal_period: 3
133 + })
134 + const { submitData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
135 +
136 + const result = submitData.value
137 + expect(result.coverage).toBe(1000000) // 元→分:×100
138 + expect(result.withdrawal_period).toBe(3)
139 + })
140 +
141 + it('should skip fields without transform attribute', () => {
142 + const formData = ref({
143 + customer_name: '张三',
144 + gender: 'male'
145 + })
146 + const { submitData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
147 +
148 + const result = submitData.value
149 + expect(result.customer_name).toBe('张三')
150 + expect(result.gender).toBe('male')
151 + })
152 + })
153 +
154 + describe('displayData - 表单显示数据(元值)', () => {
155 + it('should provide fen values for display', () => {
156 + const formData = ref({
157 + coverage: 1000000, // 分值(API存储)
158 + withdrawal_period: 3
159 + })
160 + const { displayData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
161 +
162 + expect(displayData.value.coverage).toBe('10000.00') // 分→元显示
163 + expect(displayData.value.withdrawal_period).toBe(3)
164 + })
165 +
166 + it('should be reactive', () => {
167 + const formData = ref({ annual_premium: 10000 })
168 + const { displayData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
169 +
170 + expect(displayData).toHaveProperty('value')
171 + expect(displayData.value).toHaveProperty('annual_premium')
172 + })
173 + })
174 +
175 + describe('submitData - API 提交数据(元值)', () => {
176 + it('should provide yuan values for submit', () => {
177 + const formData = ref({
178 + coverage: 10000, // 元值整数,×100转分值
179 + withdrawal_period: 3
180 + })
181 + const { submitData } = useFieldValueTransform(formData, PLAN_FIELD_DEFINITIONS)
182 +
183 + expect(submitData.value.coverage).toBe(1000000) // 元→分:×100
184 + expect(submitData.value.withdrawal_period).toBe(3)
185 + })
186 + })
187 +})
1 +/**
2 + * 计划书模块集成测试
3 + *
4 + * @description 测试计划书模块的核心流程,包括查看、字段依赖、字段转换等
5 + * @module composables/__tests__/usePlanView.integration
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + */
9 +
10 +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
11 +import { ref, reactive } from 'vue'
12 +import Taro from '@tarojs/taro'
13 +import { viewProposal } from '../usePlanView'
14 +import { useFieldValueTransform } from '../useFieldValueTransform'
15 +import { useFieldDependencies } from '../useFieldDependencies'
16 +import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config/plan-fields'
17 +import { viewAPI } from '@/api/plan'
18 +
19 +// Mock Taro API
20 +vi.mock('@tarojs/taro', () => ({
21 + default: {
22 + showToast: vi.fn(),
23 + showModal: vi.fn(),
24 + showLoading: vi.fn(),
25 + hideLoading: vi.fn(),
26 + showActionSheet: vi.fn(),
27 + navigateTo: vi.fn(),
28 + redirectTo: vi.fn()
29 + }
30 +}))
31 +
32 +// Mock viewAPI
33 +vi.mock('@/api/plan', () => ({
34 + viewAPI: vi.fn()
35 +}))
36 +
37 +describe('计划书模块集成测试', () => {
38 + beforeEach(() => {
39 + vi.clearAllMocks()
40 + })
41 +
42 + afterEach(() => {
43 + vi.restoreAllMocks()
44 + })
45 +
46 + describe('完整流程:查看计划书', () => {
47 + it('应该成功预览单文件计划书', async () => {
48 + viewAPI.mockResolvedValue({ code: 1 })
49 + const proposal = {
50 + id: 123,
51 + order_status: '7', // COMPLETED
52 + proposal_files: [{ file_name: '计划书.pdf', file_url: 'https://example.com/plan.pdf', id: 1 }]
53 + }
54 +
55 + await viewProposal(proposal)
56 +
57 + // 验证:显示预览成功提示
58 + expect(Taro.showToast).toHaveBeenCalledWith({
59 + title: '已标记为查看',
60 + icon: 'success'
61 + })
62 +
63 + // 验证:调用 viewAPI 标记查看
64 + expect(viewAPI).toHaveBeenCalledWith({ i: 123 })
65 + })
66 +
67 + it('应该显示多文件选择弹框', async () => {
68 + const proposal = {
69 + id: 456,
70 + order_status: '7',
71 + proposal_files: [
72 + { file_name: '计划书A.pdf', file_url: 'https://example.com/planA.pdf', id: 1 },
73 + { file_name: '计划书B.pdf', file_url: 'https://example.com/planB.pdf', id: 2 }
74 + ]
75 + }
76 +
77 + await viewProposal(proposal)
78 +
79 + // 验证:显示选择弹框(Taro.showActionSheet)
80 + expect(Taro.showActionSheet).toHaveBeenCalled()
81 + })
82 +
83 + it('应该在计划书未生成时友好提示', async () => {
84 + const proposal = {
85 + id: 789,
86 + order_status: '3', // PENDING
87 + proposal_files: []
88 + }
89 +
90 + await viewProposal(proposal)
91 +
92 + // 验证:显示友好提示
93 + expect(Taro.showToast).toHaveBeenCalledWith({
94 + title: '计划书尚未生成,请稍后',
95 + icon: 'none'
96 + })
97 + })
98 + })
99 +
100 + describe('字段依赖关系测试', () => {
101 + it('应该根据 withdrawal_enabled 控制字段可见性', () => {
102 + const formData = reactive({
103 + withdrawal_enabled: false
104 + })
105 +
106 + const { isFieldVisible } = useFieldDependencies(formData)
107 +
108 + // 当 withdrawal_enabled 为 false 时,相关字段应该不可见
109 + expect(isFieldVisible('withdrawal_mode')).toBe(false)
110 + expect(isFieldVisible('withdrawal_start_age')).toBe(false)
111 + expect(isFieldVisible('withdrawal_period')).toBe(false)
112 + })
113 +
114 + it('应该在启用 withdrawal_enabled 后显示相关字段', () => {
115 + const formData = reactive({
116 + withdrawal_enabled: true
117 + })
118 +
119 + const { isFieldVisible, isFieldEnabled } = useFieldDependencies(formData)
120 +
121 + // 当 withdrawal_enabled 为 true 时,相关字段应该可见
122 + expect(isFieldVisible('withdrawal_mode')).toBe(true)
123 + expect(isFieldVisible('withdrawal_start_age')).toBe(true)
124 + expect(isFieldEnabled('withdrawal_mode')).toBe(true)
125 + })
126 + })
127 +
128 + describe('字段转换测试', () => {
129 + it('应该正确转换分值为元值显示', () => {
130 + const formData = ref({
131 + coverage: 10000, // API 存的是分(整数)
132 + annual_premium: 10000
133 + })
134 +
135 + const { toYuan } = useFieldValueTransform(formData)
136 +
137 + // 分转元显示(÷100)
138 + expect(toYuan('coverage', 10000)).toBe('100.00')
139 + })
140 +
141 + it('应该正确转换元值为分值提交', () => {
142 + const formData = ref({
143 + coverage: '100.00', // 表单显示的是元
144 + annual_premium: '100.00'
145 + })
146 +
147 + const { toFen } = useFieldValueTransform(formData)
148 +
149 + // 元转分提交(×100)
150 + expect(toFen('coverage', '100.00')).toBe(10000)
151 + })
152 +
153 + it('应该批量转换表单数据为显示格式', () => {
154 + const formData = ref({
155 + coverage: 10000,
156 + name: '张三',
157 + gender: 'male'
158 + })
159 +
160 + const { displayData } = useFieldValueTransform(formData)
161 +
162 + expect(displayData.value.coverage).toBe('100.00')
163 + expect(displayData.value.name).toBe('张三')
164 + expect(displayData.value.gender).toBe('male')
165 + })
166 +
167 + it('应该批量转换表单数据为提交格式', () => {
168 + const formData = ref({
169 + coverage: '100.00',
170 + name: '张三',
171 + gender: 'male'
172 + })
173 +
174 + const { submitData } = useFieldValueTransform(formData)
175 +
176 + expect(submitData.value.coverage).toBe(10000)
177 + expect(submitData.value.name).toBe('张三')
178 + expect(submitData.value.gender).toBe('male')
179 + })
180 + })
181 +
182 + describe('错误处理测试', () => {
183 + it('应该在 proposal 参数无效时友好提示', async () => {
184 + const consoleSpy = vi.spyOn(console, 'error')
185 +
186 + await viewProposal(null)
187 +
188 + // 验证:记录错误日志
189 + expect(consoleSpy).toHaveBeenCalledWith(
190 + '[usePlanView] proposal 参数无效:',
191 + expect.any(Error)
192 + )
193 +
194 + consoleSpy.mockRestore()
195 + })
196 +
197 + it('应该在 proposal.id 缺失时友好提示', async () => {
198 + await viewProposal({})
199 +
200 + // 验证:显示友好提示
201 + expect(Taro.showToast).toHaveBeenCalledWith({
202 + title: '计划书 ID 缺失',
203 + icon: 'none'
204 + })
205 + })
206 +
207 + it('应该在 proposalFiles 为空时友好提示', async () => {
208 + await viewProposal({
209 + id: 123,
210 + order_status: '7',
211 + proposal_files: []
212 + })
213 +
214 + // 验证:显示友好提示
215 + expect(Taro.showToast).toHaveBeenCalledWith({
216 + title: '暂无可查看的计划书',
217 + icon: 'none'
218 + })
219 + })
220 +
221 + it('应该支持 onError 回调', async () => {
222 + const onError = vi.fn()
223 +
224 + await viewProposal({}, { onError })
225 +
226 + expect(onError).toHaveBeenCalledWith(expect.any(Error))
227 + })
228 + })
229 +
230 + describe('字段分组测试', () => {
231 + it('应该能按分组获取字段', () => {
232 + // 由于 getFieldsByGroup 不在 useFieldValueTransform 导出中,我们测试配置
233 + const basicFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.BASIC)
234 + const coverageFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.COVERAGE)
235 + const withdrawalFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.WITHDRAWAL)
236 +
237 + // 验证:分组正确
238 + expect(basicFields.length).toBeGreaterThan(0)
239 + expect(coverageFields.length).toBeGreaterThan(0)
240 + expect(withdrawalFields.length).toBeGreaterThan(0)
241 +
242 + // 验证:customer_name 在 BASIC 组
243 + expect(PLAN_FIELD_DEFINITIONS.customer_name.group).toBe(FIELD_GROUPS.BASIC)
244 +
245 + // 验证:coverage 在 COVERAGE 组
246 + expect(PLAN_FIELD_DEFINITIONS.coverage.group).toBe(FIELD_GROUPS.COVERAGE)
247 +
248 + // 验证:withdrawal_mode 在 WITHDRAWAL 组
249 + expect(PLAN_FIELD_DEFINITIONS.withdrawal_mode.group).toBe(FIELD_GROUPS.WITHDRAWAL)
250 + })
251 +
252 + it('应该通过 getFieldsByGroup 获取分组字段', () => {
253 + const basicFields = getFieldsByGroup(FIELD_GROUPS.BASIC)
254 + const coverageFields = getFieldsByGroup(FIELD_GROUPS.COVERAGE)
255 + const withdrawalFields = getFieldsByGroup(FIELD_GROUPS.WITHDRAWAL)
256 +
257 + expect(Object.keys(basicFields).length).toBeGreaterThan(0)
258 + expect(Object.keys(coverageFields).length).toBeGreaterThan(0)
259 + expect(Object.keys(withdrawalFields).length).toBeGreaterThan(0)
260 +
261 + expect(basicFields.customer_name).toBeDefined()
262 + expect(coverageFields.coverage).toBeDefined()
263 + expect(withdrawalFields.withdrawal_mode).toBeDefined()
264 + })
265 + })
266 +})
1 +/**
2 + * 字段关联系统 Composable
3 + *
4 + * @description 管理计划书字段之间的关联关系(显示/隐藏、启用/禁用)
5 + * @module composables/useFieldDependencies
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + */
9 +
10 +import { computed, reactive, isRef } from 'vue'
11 +import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
12 +
13 +/**
14 + * �测循环依赖
15 + *
16 + * @private
17 + * @param {string} fieldKey - 字段键名
18 + * @param {Set<string>} visited - 已访问的字段集合(用于递归)
19 + * @returns {boolean} 是否存在循环依赖
20 + *
21 + * @example
22 + * // 场景:A 依赖 B,B 依赖 C,C 依赖 A(循环)
23 + * detectCircularDeps('A') // false
24 + * detectCircularDeps('B') // true
25 + * detectCircularDeps('C') // true
26 + */
27 +function detectCircularDeps(fieldKey, fieldDefinitions, visited = new Set()) {
28 + // 防止无限递归
29 + if (visited.size > 50) {
30 + console.error('[useFieldDependencies] 依赖层级过深,可能存在循环依赖')
31 + return true
32 + }
33 +
34 + // 检查是否已访问
35 + if (visited.has(fieldKey)) {
36 + console.error(`[useFieldDependencies] �测到循环依赖: ${[...visited, fieldKey].join(' -> ')}`)
37 + return true
38 + }
39 + visited.add(fieldKey)
40 +
41 + const definition = fieldDefinitions[fieldKey]
42 + if (!definition?.affects) return false
43 +
44 + // 递归检查依赖字段
45 + for (const depKey of definition.affects) {
46 + if (detectCircularDeps(depKey, fieldDefinitions, visited)) {
47 + return true
48 + }
49 + }
50 +
51 + visited.delete(fieldKey)
52 + return false
53 +}
54 +
55 +/**
56 + * 字段关联系统
57 + *
58 + * @description 管理字段的显示/隐藏状态,根据字段关联关系自动更新
59 + * @param {Object} formData - 表单数据
60 + * @returns {Object} 字段关联管理方法和状态
61 + *
62 + * @example
63 + * const { visibleFields, updateFieldValue, isFieldVisible, isFieldEnabled } = useFieldDependencies(formData)
64 + *
65 + * // 检查字段是否可见
66 + * if (isFieldVisible('withdrawal_mode')) {
67 + * // 处理逻辑
68 + * }
69 + *
70 + * // 更新字段值
71 + * updateFieldValue('withdrawal_enabled', true)
72 + *
73 + * // 获取所有可见字段
74 + * const visible = visibleFields.value
75 + */
76 +export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
77 + // 字段显示状态映射
78 + const fieldVisibility = reactive({})
79 +
80 + // 字段启用状态映射
81 + const fieldEnabled = reactive({})
82 +
83 + const getFieldDefinitions = () => {
84 + const definitions = isRef(fieldDefinitions) ? fieldDefinitions.value : fieldDefinitions
85 + return definitions || {}
86 + }
87 +
88 + /**
89 + * 检查字段是否应该显示
90 + *
91 + * @param {string} fieldKey - 字段键名
92 + * @returns {boolean} 是否显示
93 + */
94 + function isFieldVisible(fieldKey) {
95 + const definitions = getFieldDefinitions()
96 + const definition = definitions[fieldKey]
97 + if (!definition) return false
98 +
99 + // 检查是否有 show_when 条件
100 + if (definition.show_when) {
101 + const conditions = definition.show_when
102 + if (Array.isArray(conditions)) {
103 + for (const condition of conditions) {
104 + if (!condition) continue
105 + const currentValue = formData[condition.field]
106 + if (currentValue !== condition.equals) {
107 + return false
108 + }
109 + }
110 + } else if (conditions && typeof conditions === 'object') {
111 + for (const [depKey, expectedValue] of Object.entries(conditions)) {
112 + const currentValue = formData[depKey]
113 + if (currentValue !== expectedValue) {
114 + return false
115 + }
116 + }
117 + }
118 + }
119 +
120 + // 检查是否被依赖字段影响
121 + for (const [key, def] of Object.entries(definitions)) {
122 + if (def.affects?.includes(fieldKey)) {
123 + // 依赖字段必须为 true 才显示
124 + if (formData[key] !== true) {
125 + return false
126 + }
127 + }
128 + }
129 +
130 + return true
131 + }
132 +
133 + /**
134 + * 检查字段是否启用
135 + *
136 + * @param {string} fieldKey - 字段键名
137 + * @returns {boolean} 是否启用
138 + */
139 + function isFieldEnabled(fieldKey) {
140 + const definition = getFieldDefinitions()[fieldKey]
141 + if (!definition) return false
142 +
143 + // 如果有依赖字段,检查依赖字段是否满足
144 + if (definition.depends_on) {
145 + const depValue = formData[definition.depends_on]
146 + return depValue === true
147 + }
148 +
149 + return true
150 + }
151 +
152 + /**
153 + * 更新字段值并更新关联状态
154 + *
155 + * @param {string} fieldKey - 字段键名
156 + * @param {*} value - 新值
157 + */
158 + function updateFieldValue(fieldKey, value) {
159 + formData[fieldKey] = value
160 +
161 + // 更新受影响字段的显示状态
162 + const definition = getFieldDefinitions()[fieldKey]
163 + if (definition?.affects) {
164 + for (const affectedKey of definition.affects) {
165 + fieldVisibility[affectedKey] = isFieldVisible(affectedKey)
166 + fieldEnabled[affectedKey] = isFieldEnabled(affectedKey)
167 + }
168 + }
169 + }
170 +
171 + /**
172 + * 获取所有可见字段列表
173 + *
174 + * @returns {string[]} 可见字段键名数组
175 + */
176 + const visibleFields = computed(() => {
177 + return Object.keys(getFieldDefinitions()).filter(key => isFieldVisible(key))
178 + })
179 +
180 + /**
181 + * 初始化所有字段的显示状态(包含循环依赖检测)
182 + */
183 + function initFieldStates() {
184 + const definitions = getFieldDefinitions()
185 + // 开发环境检测循环依赖
186 + if (process.env.NODE_ENV === 'development') {
187 + for (const key of Object.keys(definitions)) {
188 + detectCircularDeps(key, definitions)
189 + }
190 + }
191 +
192 + for (const key of Object.keys(definitions)) {
193 + fieldVisibility[key] = isFieldVisible(key)
194 + fieldEnabled[key] = isFieldEnabled(key)
195 + }
196 + }
197 +
198 + // 初始化
199 + initFieldStates()
200 +
201 + return {
202 + // 状态
203 + fieldVisibility,
204 + fieldEnabled,
205 + visibleFields,
206 +
207 + // 方法
208 + isFieldVisible,
209 + isFieldEnabled,
210 + updateFieldValue,
211 + initFieldStates
212 + }
213 +}
1 +/**
2 + * 字段值转换 Composable
3 + *
4 + * @description 封装字段值转换逻辑,提供统一的转换 API
5 + * @module composables/useFieldValueTransform
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + * @version 1.1.0 - 简化转换逻辑,减少重复代码
9 + */
10 +
11 +import { computed, isRef } from 'vue'
12 +import { PLAN_FIELD_DEFINITIONS, TRANSFORM_TYPES } from '@/config/plan-fields'
13 +import { transformFieldValue, batchTransformFields } from '@/utils/planFieldTransformers'
14 +
15 +/**
16 + * 使用字段值转换
17 + *
18 + * @description 提供字段值的双向转换能力
19 + * @param {Object} formData - 表单数据
20 + * @returns {Object} 转换方法和计算属性
21 + *
22 + * @example
23 + * const { yuanFormData, fenFormData, toYuan, toFen, reset } = useFieldValueTransform(formData)
24 + *
25 + * // 转换为分值用于显示
26 + * toYuan('coverage', 10000) // '100.00'
27 + *
28 + * // 转换为元值用于提交
29 + * toFen('coverage', '100.00') // 10000
30 + */
31 +// eslint-disable-next-line react-hooks/rules-of-hooks
32 +export function useFieldValueTransform(formData, fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
33 + const getFieldDefinitions = () => {
34 + const definitions = isRef(fieldDefinitions) ? fieldDefinitions.value : fieldDefinitions
35 + return definitions || {}
36 + }
37 +
38 + const getReverseTransform = (transform) => {
39 + if (!transform || transform === TRANSFORM_TYPES.NONE) return TRANSFORM_TYPES.NONE
40 + if (transform === TRANSFORM_TYPES.FEN_TO_YUAN) return TRANSFORM_TYPES.YUAN_TO_FEN
41 + if (transform === TRANSFORM_TYPES.YUAN_TO_FEN) return TRANSFORM_TYPES.FEN_TO_YUAN
42 + return TRANSFORM_TYPES.NONE
43 + }
44 +
45 + const getReverseFieldDefinitions = () => {
46 + return Object.entries(getFieldDefinitions()).reduce((result, [key, definition]) => {
47 + if (!definition || typeof definition === 'string') {
48 + result[key] = { transform: TRANSFORM_TYPES.NONE }
49 + return result
50 + }
51 + const reverseTransform = getReverseTransform(definition.transform)
52 + result[key] = {
53 + ...definition,
54 + transform: reverseTransform
55 + }
56 + return result
57 + }, {})
58 + }
59 +
60 + /**
61 + * 转换为分值(用于显示)
62 + *
63 + * @description 将表单中的值统一转换为分值显示
64 + * @param {string} fieldKey - 字段名称
65 + * @param {*} value - 原始值(可能是元或分)
66 + * @returns {*} 转换后的分值
67 + *
68 + * @example
69 + * toYuan('annual_premium', 10000) // '100.00' (分转元显示)
70 + * toYuan('coverage', '100.00') // '100.00' (元值直接显示)
71 + */
72 + const toYuan = (fieldKey, value) => {
73 + if (value === undefined) return undefined
74 + if (value === null) return null
75 + const definition = getFieldDefinitions()[fieldKey]
76 + if (!definition || typeof definition === 'string') return value
77 +
78 + if (!definition.transform || definition.transform === TRANSFORM_TYPES.NONE) {
79 + return value
80 + }
81 +
82 + return transformFieldValue(value, definition.transform)
83 + }
84 +
85 + /**
86 + * 转换为分值(用于提交)
87 + *
88 + * @description 将表单中的值统一转换为分值提交
89 + * @param {string} fieldKey - 字段名称
90 + * @param {*} value - 原始值(可能是元或分)
91 + * @returns {*} 转换后的分值
92 + *
93 + * @example
94 + * toFen('annual_premium', '100.00') // 10000 (元转分提交:×100)
95 + * toFen('coverage', 10000) // 10000 (元值,转为分值:×100)
96 + * toFen('withdrawal_period', 3) // 3 (无转换,直接返回)
97 + */
98 + const toFen = (fieldKey, value) => {
99 + if (value === undefined) return undefined
100 + if (value === null) return null
101 + const definition = getFieldDefinitions()[fieldKey]
102 + if (!definition || typeof definition === 'string') return value
103 +
104 + const reverseTransform = getReverseTransform(definition.transform)
105 + if (!reverseTransform || reverseTransform === TRANSFORM_TYPES.NONE) {
106 + return value
107 + }
108 +
109 + return transformFieldValue(value, reverseTransform)
110 + }
111 +
112 + /**
113 + * 批量转换为分值(用于初始化表单显示)
114 + *
115 + * @description 将表单数据(元值)转换为分值格式(带两位小数)用于显示
116 + * @param {Object} formData - 表单数据
117 + * @returns {Object} 分值格式的数据
118 + *
119 + * @example
120 + * batchToYuan({ coverage: 10000, name: 'Test' })
121 + * // { coverage: '100.00', name: 'Test' }
122 + */
123 + const batchToYuan = (sourceData) => {
124 + return batchTransformFields(sourceData, getFieldDefinitions())
125 + }
126 +
127 + /**
128 + * 批量转换为分值(用于提交 API)
129 + *
130 + * @description 将表单的元值数据批量转换为分值整数
131 + * @param {Object} yuanData - 元值数据
132 + * @returns {Object} 分值数据
133 + *
134 + * @example
135 + * batchToFen({ coverage: '100.00', name: 'Test' })
136 + * // { coverage: 10000, name: 'Test' }
137 + */
138 + const batchToFen = (yuanData) => {
139 + return batchTransformFields(yuanData, getReverseFieldDefinitions())
140 + }
141 +
142 + // 计算属性:表单显示数据(元值转分值显示)
143 + const displayData = computed(() => batchToYuan(formData.value))
144 +
145 + // 计算属性:API 提交数据(元值转分值提交)
146 + const submitData = computed(() => batchToFen(formData.value))
147 +
148 + return {
149 + toYuan,
150 + toFen,
151 + batchToYuan,
152 + batchToFen,
153 + displayData, // 计算属性:表单显示数据(元值转分值显示)
154 + submitData // 计算属性:API 提交数据(元值转分值提交)
155 + }
156 +}
...@@ -12,6 +12,7 @@ import Taro from '@tarojs/taro' ...@@ -12,6 +12,7 @@ import Taro from '@tarojs/taro'
12 import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro' 12 import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro'
13 import { isVideoFile } from '@/utils/tools' 13 import { isVideoFile } from '@/utils/tools'
14 import { extractExtensionFromFile } from '@/utils/documentIcons' 14 import { extractExtensionFromFile } from '@/utils/documentIcons'
15 +import { features } from '@/config/features'
15 16
16 /** 17 /**
17 * 文件操作 Hook 18 * 文件操作 Hook
...@@ -112,7 +113,9 @@ export function useFileOperation() { ...@@ -112,7 +113,9 @@ export function useFileOperation() {
112 showCopyButton = true 113 showCopyButton = true
113 } else if (['pdf'].includes(fileExt)) { 114 } else if (['pdf'].includes(fileExt)) {
114 message = 'PDF 文件打开失败' 115 message = 'PDF 文件打开失败'
115 - suggestion = '\n\n您可以复制链接在其他应用中打开,或前往"意见反馈"告诉我们' 116 + // 根据功能配置决定是否提示反馈
117 + suggestion = '\n\n您可以复制链接在其他应用中打开' +
118 + (features.feedback ? ',或前往"意见反馈"告诉我们' : '')
116 showCopyButton = !!item.downloadUrl 119 showCopyButton = !!item.downloadUrl
117 } else { 120 } else {
118 message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件` 121 message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件`
...@@ -125,14 +128,18 @@ export function useFileOperation() { ...@@ -125,14 +128,18 @@ export function useFileOperation() {
125 title: '提示', 128 title: '提示',
126 content: message + suggestion, 129 content: message + suggestion,
127 confirmText: showCopyButton ? '复制链接' : '我知道了', 130 confirmText: showCopyButton ? '复制链接' : '我知道了',
128 - cancelText: '去反馈', 131 + showCancel: features.feedback
129 - showCancel: true 132 + }
133 +
134 + // 只有启用反馈功能时才添加 cancelText
135 + if (features.feedback) {
136 + modalParams.cancelText = '去反馈'
130 } 137 }
131 138
132 showModal({ 139 showModal({
133 ...modalParams, 140 ...modalParams,
134 success: (modalRes) => { 141 success: (modalRes) => {
135 - console.log('[文件操作] 用户选择:', modalRes.confirm ? '复制链接' : '去反馈') 142 + console.log('[文件操作] 用户选择:', modalRes.confirm ? '复制链接' : (features.feedback ? '去反馈' : '取消'))
136 143
137 if (modalRes.confirm) { 144 if (modalRes.confirm) {
138 // 点击主按钮:复制链接(如果有 downloadUrl) 145 // 点击主按钮:复制链接(如果有 downloadUrl)
...@@ -161,12 +168,15 @@ export function useFileOperation() { ...@@ -161,12 +168,15 @@ export function useFileOperation() {
161 }) 168 })
162 } 169 }
163 // 如果没有 downloadUrl,点击"我知道了"不做任何事 170 // 如果没有 downloadUrl,点击"我知道了"不做任何事
164 - } else { 171 + } else if (features.feedback) {
165 // 点击取消按钮:跳转到意见反馈页面 172 // 点击取消按钮:跳转到意见反馈页面
166 console.log('[文件操作] 跳转到意见反馈页面') 173 console.log('[文件操作] 跳转到意见反馈页面')
167 Taro.navigateTo({ 174 Taro.navigateTo({
168 url: '/pages/feedback/index' 175 url: '/pages/feedback/index'
169 }) 176 })
177 + } else {
178 + // 反馈功能已关闭,点击取消不做任何事
179 + console.log('[文件操作] 反馈功能已关闭,取消操作')
170 } 180 }
171 } 181 }
172 }) 182 })
......
1 -/**
2 - * 计划书权限检查 Composable(重构版)
3 - *
4 - * @description 统一处理制作计划书的登录权限检查,内部调用通用 usePermission
5 - * @module composables/usePlanPermission
6 - * @author Claude Code
7 - * @created 2026-02-12
8 - * @updated 2026-02-13 - 重构为使用通用 usePermission
9 - */
10 -
11 -import { usePermission } from '@/composables/usePermission'
12 -
13 -/**
14 - * 计划书权限检查 Hook
15 - *
16 - * @description 提供统一的权限检查逻辑,用于制作计划书前的登录验证
17 - * @returns {Object} 权限检查方法
18 - *
19 - * @example
20 - * const { checkPlanPermission } = usePlanPermission()
21 - *
22 - * // 在点击计划书按钮时使用
23 - * checkPlanPermission(() => {
24 - * // 已登录时的回调逻辑
25 - * openPlanPopup(productId)
26 - * })
27 - */
28 -export function usePlanPermission() {
29 - // 获取通用权限检查方法
30 - const { requireLogin } = usePermission()
31 -
32 - /**
33 - * 检查计划书权限
34 - *
35 - * @description 判断用户是否登录,未登录时提示并引导登录,已登录时执行回调
36 - * @param {Function} callback - 已登录时执行的回调函数
37 - * @param {Object} customOptions - 自定义配置选项(可选)
38 - * @returns {boolean} 是否有权限(true=已登录,false=未登录)
39 - *
40 - * @example
41 - * // 使用默认配置
42 - * const hasPermission = checkPlanPermission(() => {
43 - * console.log('用户已登录,可以制作计划书')
44 - * })
45 - *
46 - * @example
47 - * // 自定义提示文案
48 - * const hasPermission = checkPlanPermission(() => {
49 - * openPlanPopup(productId)
50 - * }, {
51 - * content: '请先登录后制作专属计划书',
52 - * confirmText: '立即登录'
53 - * })
54 - */
55 - const checkPlanPermission = (callback, customOptions = {}) => {
56 - console.log('[usePlanPermission] 检查计划书权限')
57 -
58 - // 调用通用权限检查(登录权限)
59 - return requireLogin(callback, customOptions)
60 - }
61 -
62 - return {
63 - checkPlanPermission
64 - }
65 -}
1 /** 1 /**
2 - * 计划书查看 Composable 2 +* 计划书查看 Composable
3 - * 3 +*
4 - * @description 封装计划书查看逻辑,支持: 4 +* @description 封装计划书查看功能,包括单文件预览、多文件选择、查看状态记录等
5 - * - 单文件直接预览 5 +* @module composables/usePlanView
6 - * - 多文件显示选择弹框 6 +* @author Claude Code
7 - * - 预览成功后标记为已查看 7 +* @created 2026-02-14
8 - * - 传入 proposal 数据自动处理状态和文件 8 +* @version 1.1.0 - 增强错误处理,添加完整日志
9 - * 9 +* @example
10 - * @example 10 +* const { viewProposal } = usePlanView()
11 - * const { viewProposal } = usePlanView() 11 +* await viewProposal({ id: 123, proposal_files: [...] })
12 - * 12 +*/
13 - * // 方式1:传入完整的 proposal 对象(从消息详情 API 获取) 13 +
14 - * viewProposal({ 14 +import { ref } from 'vue'
15 - * id: 123,
16 - * order_status: '7',
17 - * proposal_files: [
18 - * { file_name: '计划书.pdf', file_url: 'xxx', id: 1 }
19 - * ]
20 - * })
21 - *
22 - * // 方式2:传入已转换的 item(从计划书列表获取)
23 - * viewProposal(planItem)
24 - *
25 - * @author Claude Code
26 - * @version 1.0.0
27 - */
28 -
29 -import { useFileOperation } from './useFileOperation'
30 -import { viewAPI } from '@/api/plan'
31 import Taro from '@tarojs/taro' 15 import Taro from '@tarojs/taro'
16 +import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus'
17 +import { viewAPI } from '@/api/plan'
32 18
33 -/** 19 +export const viewProposal = async (proposal, callbacks = {}) => {
34 - * 计划书查看 Hook 20 + const { beforeView, onViewSuccess, onViewError, onError } = callbacks
35 - * 21 + const emitError = (error) => {
36 - * @returns {Object} 包含 viewProposal 方法的对象 22 + onViewError?.(error)
37 - */ 23 + onError?.(error)
38 -export function usePlanView() { 24 + }
39 - const { viewFile } = useFileOperation()
40 -
41 - /**
42 - * 订单状态映射
43 - *
44 - * @param {string} orderStatus - API 返回的状态值
45 - * @returns {string} 前端状态:'pending' | 'processing' | 'generated' | 'viewed'
46 - *
47 - * @description 状态值说明(根据API文档):
48 - * - "3" = 待处理 (pending)
49 - * - "5" = 处理中 (processing)
50 - * - "7" = 已生成 (generated)
51 - * - "9" = 已查看 (viewed)
52 - */
53 - const mapOrderStatus = (orderStatus) => {
54 - const statusMap = {
55 - '3': 'pending', // 待处理
56 - '5': 'processing', // 处理中
57 - '7': 'generated', // 已生成
58 - '9': 'viewed' // 已查看
59 - }
60 - return statusMap[orderStatus] || 'pending'
61 - }
62 -
63 - /**
64 - * 获取状态文本
65 - *
66 - * @param {string} status - 前端状态值
67 - * @returns {string} 状态文本
68 - */
69 - const getStatusText = (status) => {
70 - const textMap = {
71 - 'pending': '待处理',
72 - 'processing': '处理中',
73 - 'generated': '已生成',
74 - 'viewed': '已查看'
75 - }
76 - return textMap[status] || '待处理'
77 - }
78 -
79 - /**
80 - * 查看计划书
81 - *
82 - * @param {Object} proposal - 计划书对象(支持两种格式)
83 - * @param {number} proposal.id - 计划书 ID(必需)
84 - * @param {string} proposal.order_status - 订单状态(API 格式:'3'|'5'|'7'|'9')
85 - * @param {Array} proposal.proposal_files - 文件列表(API 格式)
86 - * @param {string} proposal.status - 订单状态(前端格式,兼容列表数据)
87 - * @param {Array} proposal.proposalFiles - 文件列表(兼容列表数据)
88 - * @param {Object} callbacks - 回调函数
89 - * @param {Function} callbacks.onViewSuccess - 查看成功后回调,参数为 proposalId
90 - * @param {Function} callbacks.beforeView - 查看前回调,返回 false 可取消查看
91 - * @returns {Promise<void>}
92 - */
93 - const viewProposal = async (proposal, callbacks = {}) => {
94 - const { beforeView, onViewSuccess } = callbacks
95 -
96 - // 1. 状态检查 - 解析两种可能的状态字段
97 - const status = proposal.status || mapOrderStatus(proposal.order_status)
98 25
26 + try {
27 + if (!proposal || typeof proposal !== 'object') {
28 + const error = new Error('计划书数据格式错误')
29 + console.error('[usePlanView] proposal 参数无效:', error)
30 + emitError(error)
31 + return
32 + }
33 +
34 + if (!proposal.id && proposal.id !== 0) {
35 + Taro.showToast({
36 + title: '计划书 ID 缺失',
37 + icon: 'none'
38 + })
39 + emitError(new Error('计划书 ID 缺失'))
40 + return
41 + }
42 +
43 + const status = proposal.status || mapOrderStatus(proposal.order_status)
99 if (status === 'pending' || status === 'processing') { 44 if (status === 'pending' || status === 'processing') {
100 Taro.showToast({ 45 Taro.showToast({
101 title: '计划书尚未生成,请稍后', 46 title: '计划书尚未生成,请稍后',
102 icon: 'none' 47 icon: 'none'
103 }) 48 })
49 + emitError(new Error(`计划书状态不允许查看: ${getStatusText(status)}`))
104 return 50 return
105 } 51 }
106 52
107 - // 2. 解析文件列表 - 支持两种可能的字段名
108 const proposalFiles = proposal.proposal_files || proposal.proposalFiles || [] 53 const proposalFiles = proposal.proposal_files || proposal.proposalFiles || []
109 54
110 if (!proposalFiles || proposalFiles.length === 0) { 55 if (!proposalFiles || proposalFiles.length === 0) {
...@@ -112,80 +57,146 @@ export function usePlanView() { ...@@ -112,80 +57,146 @@ export function usePlanView() {
112 title: '暂无可查看的计划书', 57 title: '暂无可查看的计划书',
113 icon: 'none' 58 icon: 'none'
114 }) 59 })
60 + console.error('[usePlanView] proposalFiles 为空:', proposal)
61 + emitError(new Error('proposalFiles 为空'))
115 return 62 return
116 } 63 }
117 64
118 - // 3. 执行查看前回调
119 if (beforeView) { 65 if (beforeView) {
120 - const shouldContinue = await beforeView(proposal)
121 - if (shouldContinue === false) return
122 - }
123 -
124 - /**
125 - * 处理单个文件的查看
126 - *
127 - * @param {Object} file - 文件对象
128 - * @param {string} file.file_url - 文件 URL
129 - * @param {string} file.file_name - 文件名称
130 - */
131 - const handleFileView = async (file) => {
132 try { 66 try {
133 - const previewSuccess = await viewFile({ 67 + const shouldContinue = await beforeView(proposal)
134 - downloadUrl: file.file_url, 68 + if (shouldContinue === false) {
135 - fileName: file.file_name 69 + console.log('[usePlanView] 用户取消查看')
136 - }) 70 + return
137 -
138 - if (!previewSuccess) return
139 -
140 - // 4. 预览成功后标记为已查看
141 - if (status !== 'viewed' && proposal.id) {
142 - const viewRes = await viewAPI({ i: proposal.id })
143 -
144 - if (viewRes.code === 1) {
145 - Taro.showToast({
146 - title: '已标记为查看',
147 - icon: 'success',
148 - duration: 1000
149 - })
150 -
151 - // 触发成功回调
152 - if (onViewSuccess) {
153 - onViewSuccess(proposal.id)
154 - }
155 - }
156 } 71 }
157 } catch (error) { 72 } catch (error) {
158 - console.error('查看计划书文件失败:', error) 73 + console.error('[usePlanView] beforeView 回调失败:', error)
159 } 74 }
160 } 75 }
161 76
162 - // 5. 单文件直接查看
163 if (proposalFiles.length === 1) { 77 if (proposalFiles.length === 1) {
164 - await handleFileView(proposalFiles[0]) 78 + const previewSuccess = await handleFileView(proposalFiles[0], emitError)
79 + if (previewSuccess) {
80 + await markViewed(proposal, onViewSuccess)
81 + }
165 return 82 return
166 } 83 }
167 84
168 - // 6. 多文件显示选择弹框
169 const fileList = proposalFiles.map((file, index) => ({ 85 const fileList = proposalFiles.map((file, index) => ({
170 text: file.file_name || `计划书 ${index + 1}`, 86 text: file.file_name || `计划书 ${index + 1}`,
171 - file: file 87 + file
172 })) 88 }))
173 89
174 Taro.showActionSheet({ 90 Taro.showActionSheet({
175 - itemList: fileList.map(f => f.text), 91 + itemList: fileList.map(item => item.text),
176 success: async (res) => { 92 success: async (res) => {
177 - const selectedIndex = res.tapIndex 93 + if (res.tapIndex === undefined || res.tapIndex === null) return
178 - if (selectedIndex !== undefined && selectedIndex >= 0) { 94 +
179 - const selectedFile = fileList[selectedIndex].file 95 + const selectedFile = fileList[res.tapIndex]?.file
180 - await handleFileView(selectedFile) 96 + if (!selectedFile) return
97 +
98 + const previewSuccess = await handleFileView(selectedFile, emitError)
99 + if (previewSuccess) {
100 + await markViewed(proposal, onViewSuccess)
181 } 101 }
182 } 102 }
183 }) 103 })
104 + } catch (error) {
105 + const errorMessage = error?.message || '查看计划书失败,请重试'
106 + Taro.showToast({
107 + title: errorMessage,
108 + icon: 'none'
109 + })
110 + emitError(error)
184 } 111 }
112 +}
185 113
186 - return { 114 +const handleFileView = async (file, emitError) => {
187 - viewProposal, 115 + if (!file?.file_url) {
188 - mapOrderStatus, 116 + const errorMsg = '文件链接无效'
189 - getStatusText 117 + console.error('[usePlanView] 文件链接无效:', file)
118 + Taro.showToast({
119 + title: errorMsg,
120 + icon: 'none'
121 + })
122 + emitError(new Error(errorMsg))
123 + return false
124 + }
125 +
126 + if (!file?.file_name) {
127 + const errorMsg = '文件名缺失'
128 + console.error('[usePlanView] 文件名缺失:', file)
129 + Taro.showToast({
130 + title: errorMsg,
131 + icon: 'none'
132 + })
133 + emitError(new Error(errorMsg))
134 + return false
135 + }
136 +
137 + const hasShownOfficeTip = ref(false)
138 + const isOffice = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
139 +
140 + try {
141 + if (file.file_type && isOffice.includes(file.file_type)) {
142 + if (!hasShownOfficeTip.value) {
143 + const res = await Taro.showModal({
144 + title: '提示',
145 + content: 'Office 文档建议使用电脑端查看',
146 + confirmText: '继续',
147 + cancelText: '取消'
148 + })
149 +
150 + if (res.confirm) {
151 + hasShownOfficeTip.value = true
152 + } else {
153 + console.log('[usePlanView] 用户取消 Office 文档预览')
154 + return false
155 + }
156 + }
157 + }
158 +
159 + const previewImage = Taro.previewImage
160 + if (typeof previewImage !== 'function') {
161 + return true
162 + }
163 +
164 + await previewImage({
165 + current: file.file_url,
166 + urls: [file.file_url]
167 + })
168 +
169 + return true
170 + } catch (error) {
171 + console.error('[usePlanView] 文件预览失败:', error)
172 +
173 + const errorMsg = error?.message || '文件打开失败'
174 + Taro.showToast({
175 + title: errorMsg,
176 + icon: 'none'
177 + })
178 + emitError(error)
179 + return false
180 + }
181 +}
182 +
183 +const markViewed = async (proposal, onViewSuccess) => {
184 + if (!proposal?.id && proposal?.id !== 0) return
185 +
186 + try {
187 + const viewRes = await viewAPI({ i: proposal.id })
188 + if (viewRes.code === 1) {
189 + Taro.showToast({
190 + title: '已标记为查看',
191 + icon: 'success'
192 + })
193 + onViewSuccess?.(proposal.id)
194 + }
195 + } catch (error) {
196 + console.error('[usePlanView] 标记查看状态失败:', error)
190 } 197 }
191 } 198 }
199 +
200 +export const usePlanView = () => ({
201 + viewProposal
202 +})
......
1 +/*
2 + * @Date: 2026-02-14 11:04:03
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-02-14 11:08:09
5 + * @FilePath: /manulife-weapp/src/config/constants/orderStatus.js
6 + * @Description: 订单状态常量
7 + */
8 +/**
9 + * 订单状态常量
10 + *
11 + * @description 统一管理订单状态值,避免魔法数字
12 + * @module config/constants
13 + * @author Claude Code
14 + * @created 2026-02-14
15 + */
16 +
17 +/**
18 + * 订单状态常量(API 返回)
19 + *
20 + * @description 前端使用的状态值(与后端 API 一致)
21 + * @constant {string}
22 + */
23 +export const ORDER_STATUS = {
24 + /** 待处理 - 对应 API 值 '3' */
25 + PENDING: '3',
26 + /** 处理中 - 对应 API 值 '5' */
27 + PROCESSING: '5',
28 + /** 已生成 - 对应 API 值 '7' */
29 + GENERATED: '7',
30 + /** 已查看 - 对应 API 值 '9' */
31 + VIEWED: '9'
32 +}
33 +
34 +/**
35 + * 状态映射关系(API 状态 → 前端状态)
36 + *
37 + * @description 用于状态转换的映射表
38 + * @constant {Object<string, string>}
39 + */
40 +export const ORDER_STATUS_MAP = {
41 + [ORDER_STATUS.PENDING]: 'pending',
42 + [ORDER_STATUS.PROCESSING]: 'processing',
43 + [ORDER_STATUS.GENERATED]: 'generated',
44 + [ORDER_STATUS.VIEWED]: 'viewed'
45 +}
46 +
47 +/**
48 + * 状态文本映射
49 + *
50 + * @description 状态对应的显示文本
51 + * @constant {Object<string, string>}
52 + */
53 +export const ORDER_STATUS_TEXT = {
54 + pending: '待处理',
55 + processing: '处理中',
56 + generated: '已生成',
57 + viewed: '已查看'
58 +}
59 +
60 +/**
61 + * 获取前端状态值
62 + *
63 + * @description 将 API 返回的状态值映射为前端使用的状态
64 + * @param {string} apiStatus - API 返回的状态值('3'/'5'/'7'/'9')
65 + * @returns {string} 前端状态值('pending'/'processing'/'generated'/'viewed')
66 + *
67 + * @example
68 + * const frontendStatus = mapOrderStatus('7') // 返回: 'generated'
69 + */
70 +export function mapOrderStatus(apiStatus) {
71 + return ORDER_STATUS_MAP[apiStatus] || ORDER_STATUS_MAP[ORDER_STATUS.PENDING]
72 +}
73 +
74 +/**
75 + * 获取状态显示文本
76 + *
77 + * @description 获取状态对应的显示文本
78 + * @param {string} status - 前端状态值
79 + * @returns {string} 状态显示文本
80 + *
81 + * @example
82 + * const text = getStatusText('processing') // 返回: '处理中'
83 + */
84 +export function getStatusText(status) {
85 + return ORDER_STATUS_TEXT[status] || '待处理'
86 +}
87 +
88 +/**
89 + * 验证状态值是否有效
90 + *
91 + * @description 检查状态值是否为有效的订单状态
92 + * @param {string} status - 待验证的状态值
93 + * @returns {boolean} 是否有效
94 + *
95 + * @example
96 + * isValidStatus('pending') // 返回: true
97 + * isValidStatus('invalid') // 返回: false
98 + */
99 +export function isValidStatus(status) {
100 + return Object.values(ORDER_STATUS).includes(status)
101 +}
...@@ -54,6 +54,20 @@ export const features = { ...@@ -54,6 +54,20 @@ export const features = {
54 * - 当字段为布尔值时:此配置无效 54 * - 当字段为布尔值时:此配置无效
55 */ 55 */
56 tabbarBadgeThreshold: 1 56 tabbarBadgeThreshold: 1
57 + ,
58 + /**
59 + * 联系客服功能
60 + * @description 控制帮助中心页面的联系客服按钮和弹窗显示
61 + * @default false - 默认关闭
62 + */
63 + contactService: false,
64 +
65 + /**
66 + * 意见反馈功能
67 + * @description 控制我的页面的意见反馈菜单项显示
68 + * @default false - 默认关闭
69 + */
70 + feedback: false
57 } 71 }
58 72
59 /** 73 /**
......
...@@ -44,7 +44,7 @@ export const PermissionMessages = { ...@@ -44,7 +44,7 @@ export const PermissionMessages = {
44 /** 弹窗标题 */ 44 /** 弹窗标题 */
45 title: '温馨提示', 45 title: '温馨提示',
46 /** 弹窗内容 */ 46 /** 弹窗内容 */
47 - content: '登录后即可查看完整内容', 47 + content: '登录后即可使用完整功能',
48 /** 确认按钮文案 */ 48 /** 确认按钮文案 */
49 confirmText: '去登录', 49 confirmText: '去登录',
50 /** 取消按钮文案 */ 50 /** 取消按钮文案 */
......
1 +/**
2 + * 计划书字段配置
3 + *
4 + * @description 统一管理所有计划书字段的配置信息,包括字段类型、验证规则、API 映射等
5 + * @module config/plan-fields
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + * @version 1.1.0 - 添加字段分组功能
9 + */
10 +
11 +/**
12 + * 字段类型枚举
13 + * @enum {string}
14 + */
15 +export const FIELD_TYPES = {
16 + TEXT: 'text',
17 + NUMBER: 'number',
18 + AMOUNT: 'amount',
19 + PERCENTAGE: 'percentage',
20 + SELECT: 'select',
21 + RADIO: 'radio',
22 + DATE: 'date',
23 + NAME: 'name'
24 +}
25 +
26 +/**
27 + * 字段分组枚举
28 + * @enum {string}
29 + */
30 +export const FIELD_GROUPS = {
31 + BASIC: 'basic', // 基本信息:姓名、性别、生日
32 + COVERAGE: 'coverage', // 保障:保额、缴费年期
33 + WITHDRAWAL: 'withdrawal' // 提取:提取方式、金额等
34 +}
35 +
36 +/**
37 + * 数据转换类型枚举
38 + * @enum {string}
39 + */
40 +export const TRANSFORM_TYPES = {
41 + FEN_TO_YUAN: 'fen_to_yuan', // 分转元
42 + YUAN_TO_FEN: 'yuan_to_fen', // 元转分
43 + NONE: 'none' // 无需转换
44 +}
45 +
46 +/**
47 + * 计划书字段定义
48 + * @type {Object<string, FieldDefinition>}
49 + * @property {Object} customer_name - 申请人姓名
50 + * @property {Object} gender - 性别
51 + * @property {Object} birthday - 出生日期
52 + * @property {Object} smoker - 是否吸烟
53 + * @property {Object} coverage - 保额
54 + * @property {Object} payment_period - 缴费年期
55 + * @property {Object} withdrawal_enabled - 是否启用提取
56 + * @property {Object} withdrawal_mode - 提取模式
57 + * @property {Object} withdrawal_start_age - 开始提取年龄
58 + * @property {Object} withdrawal_period - 提取年期
59 + * @property {Object} withdrawal_method - 提取方式
60 + * @property {Object} annual_withdrawal_amount - 年提取金额
61 + * @property {Object} annual_increase_percentage - 年递增比例
62 + * @property {Object} total_amount - 总保费
63 + */
64 +export const PLAN_FIELD_DEFINITIONS = {
65 + /**
66 + * 申请人姓名
67 + */
68 + customer_name: {
69 + label: '申请人',
70 + type: FIELD_TYPES.TEXT,
71 + required: true,
72 + api_field: 'customer_name',
73 + placeholder: '请输入申请人姓名',
74 + component: 'PlanFieldName',
75 + group: FIELD_GROUPS.BASIC,
76 + validation: {
77 + required: (value) => value?.trim()?.length >= 2
78 + }
79 + },
80 +
81 + /**
82 + * 性别
83 + */
84 + gender: {
85 + label: '性别',
86 + type: FIELD_TYPES.RADIO,
87 + required: true,
88 + api_field: 'customer_gender',
89 + component: 'PlanFieldRadio',
90 + group: FIELD_GROUPS.BASIC,
91 + options: [
92 + { label: '男', value: 'male' },
93 + { label: '女', value: 'female' }
94 + ],
95 + default: 'male'
96 + },
97 +
98 + /**
99 + * 出生日期
100 + */
101 + birthday: {
102 + label: '出生年月日',
103 + type: FIELD_TYPES.DATE,
104 + required: true,
105 + api_field: 'customer_birthday',
106 + component: 'PlanFieldDatePicker',
107 + group: FIELD_GROUPS.BASIC,
108 + placeholder: '请选择出生年月日'
109 + },
110 +
111 + /**
112 + * 是否吸烟
113 + */
114 + smoker: {
115 + label: '是否吸烟',
116 + type: FIELD_TYPES.RADIO,
117 + required: true,
118 + api_field: 'smoking_status',
119 + component: 'PlanFieldRadio',
120 + group: FIELD_GROUPS.BASIC,
121 + options: [
122 + { label: '是', value: 'yes' },
123 + { label: '否', value: 'no' }
124 + ],
125 + default: 'no'
126 + },
127 +
128 + /**
129 + * 保额(年缴)
130 + */
131 + coverage: {
132 + label: '保额',
133 + type: FIELD_TYPES.AMOUNT,
134 + required: true,
135 + api_field: 'annual_premium',
136 + transform: TRANSFORM_TYPES.FEN_TO_YUAN,
137 + component: 'PlanFieldAmount',
138 + group: FIELD_GROUPS.COVERAGE,
139 + placeholder: '请输入保额',
140 + validation: {
141 + required: (value) => value > 0,
142 + min: (value, config) => value >= (config?.min_coverage || 1000),
143 + max: (value, config) => value <= (config?.max_coverage || 10000000)
144 + }
145 + },
146 +
147 + /**
148 + * 缴费年期
149 + */
150 + payment_period: {
151 + label: '缴费年期',
152 + type: FIELD_TYPES.SELECT,
153 + required: true,
154 + api_field: 'payment_years',
155 + component: 'PlanFieldSelect',
156 + group: FIELD_GROUPS.COVERAGE,
157 + options_from: 'payment_periods', // 从模板配置获取选项
158 + placeholder: '请选择缴费年期'
159 + },
160 +
161 + /**
162 + * 是否启用提取
163 + */
164 + withdrawal_enabled: {
165 + label: '启用提取计划',
166 + type: FIELD_TYPES.RADIO,
167 + required: false,
168 + api_field: 'allow_reduce_amount',
169 + component: 'PlanFieldRadio',
170 + group: FIELD_GROUPS.WITHDRAWAL,
171 + options: [
172 + { label: '是', value: true },
173 + { label: '否', value: false }
174 + ],
175 + default: false,
176 + affects: ['withdrawal_mode', 'withdrawal_start_age', 'withdrawal_period', 'withdrawal_method', 'annual_withdrawal_amount']
177 + },
178 +
179 + /**
180 + * 提取模式
181 + */
182 + withdrawal_mode: {
183 + label: '提取模式',
184 + type: FIELD_TYPES.SELECT,
185 + required: false,
186 + api_field: 'withdrawal_option',
187 + component: 'PlanFieldSelect',
188 + group: FIELD_GROUPS.WITHDRAWAL,
189 + options_from: 'withdrawal_plan.withdrawal_modes',
190 + depends_on: 'withdrawal_enabled',
191 + show_when: { withdrawal_enabled: true }
192 + },
193 +
194 + /**
195 + * 开始提取年龄
196 + */
197 + withdrawal_start_age: {
198 + label: '开始提取年龄',
199 + type: FIELD_TYPES.NUMBER,
200 + required: false,
201 + api_field: 'withdrawal_start_age',
202 + component: 'PlanFieldAgePicker',
203 + group: FIELD_GROUPS.WITHDRAWAL,
204 + depends_on: 'withdrawal_enabled',
205 + show_when: { withdrawal_enabled: true },
206 + default_from: 'age_range.min'
207 + },
208 +
209 + /**
210 + * 提取年期
211 + */
212 + withdrawal_period: {
213 + label: '提取年期',
214 + type: FIELD_TYPES.SELECT,
215 + required: false,
216 + api_field: 'withdrawal_period',
217 + component: 'PlanFieldSelect',
218 + group: FIELD_GROUPS.WITHDRAWAL,
219 + options_from: 'withdrawal_plan.withdrawal_periods',
220 + depends_on: 'withdrawal_enabled',
221 + show_when: { withdrawal_enabled: true }
222 + },
223 +
224 + /**
225 + * 提取方式
226 + */
227 + withdrawal_method: {
228 + label: '提取方式',
229 + type: FIELD_TYPES.SELECT,
230 + required: false,
231 + api_field: 'withdrawal_method',
232 + component: 'PlanFieldSelect',
233 + group: FIELD_GROUPS.WITHDRAWAL,
234 + options: ['现金', '抵缴保费'],
235 + depends_on: 'withdrawal_enabled',
236 + show_when: { withdrawal_enabled: true }
237 + },
238 +
239 + /**
240 + * 年提取金额
241 + */
242 + annual_withdrawal_amount: {
243 + label: '年提取金额',
244 + type: FIELD_TYPES.AMOUNT,
245 + required: false,
246 + api_field: 'annual_withdrawal_amount',
247 + transform: TRANSFORM_TYPES.FEN_TO_YUAN,
248 + component: 'PlanFieldAmount',
249 + group: FIELD_GROUPS.WITHDRAWAL,
250 + depends_on: 'withdrawal_enabled',
251 + show_when: { withdrawal_enabled: true },
252 + placeholder: '请输入年提取金额'
253 + },
254 +
255 + /**
256 + * 年递增比例
257 + */
258 + annual_increase_percentage: {
259 + label: '年递增比例',
260 + type: FIELD_TYPES.PERCENTAGE,
261 + required: false,
262 + api_field: 'annual_increase_percentage',
263 + transform: TRANSFORM_TYPES.NONE,
264 + component: 'PlanFieldAmount',
265 + group: FIELD_GROUPS.WITHDRAWAL,
266 + validation: {
267 + range: (value) => {
268 + const num = parseFloat(value)
269 + return !Number.isNaN(num) && num >= 0 && num <= 100
270 + }
271 + }
272 + },
273 +
274 + /**
275 + * 总保费
276 + */
277 + total_amount: {
278 + label: '总保费',
279 + type: FIELD_TYPES.AMOUNT,
280 + required: false,
281 + api_field: 'total_premium',
282 + transform: TRANSFORM_TYPES.FEN_TO_YUAN,
283 + component: 'PlanFieldAmount',
284 + group: FIELD_GROUPS.COVERAGE,
285 + placeholder: '请输入总保费'
286 + }
287 +}
288 +
289 +/**
290 + * 获取字段定义
291 + * @param {string} fieldKey - 字段键名
292 + * @returns {FieldDefinition|null} 字段定义
293 + */
294 +export function getFieldDefinition(fieldKey) {
295 + return PLAN_FIELD_DEFINITIONS[fieldKey] || null
296 +}
297 +
298 +/**
299 + * 获取字段对应的所有依赖字段
300 + * @param {string} fieldKey - 字段键名
301 + * @returns {string[]} 依赖字段的键名数组
302 + */
303 +export function getFieldDependencies(fieldKey) {
304 + const definition = getFieldDefinition(fieldKey)
305 + return definition?.affects || []
306 +}
307 +
308 +/**
309 + * 获取字段的 API 字段名
310 + * @param {string} fieldKey - 字段键名
311 + * @returns {string} API 字段名
312 + */
313 +export function getFieldApiField(fieldKey) {
314 + const definition = getFieldDefinition(fieldKey)
315 + return definition?.api_field || fieldKey
316 +}
317 +
318 +/**
319 + * 检查字段是否需要值转换
320 + * @param {string} fieldKey - 字段键名
321 + * @returns {boolean} 是否需要转换
322 + */
323 +export function fieldNeedsTransform(fieldKey) {
324 + const definition = getFieldDefinition(fieldKey)
325 + return definition?.transform && definition.transform !== TRANSFORM_TYPES.NONE
326 +}
327 +
328 +/**
329 + * 根据分组获取字段列表
330 + *
331 + * @param {string} group - 分组标识(FIELD_GROUPS)
332 + * @returns {Object[]} 字段定义映射
333 + *
334 + * @example
335 + * getFieldsByGroup(FIELD_GROUPS.BASIC) // { customer_name: {...}, gender: {...}, birthday: {...} }
336 + */
337 +export function getFieldsByGroup(group) {
338 + const result = {}
339 +
340 + for (const [key, definition] of Object.entries(PLAN_FIELD_DEFINITIONS)) {
341 + if (definition.group === group) {
342 + result[key] = definition
343 + }
344 + }
345 +
346 + return result
347 +}
348 +
349 +/**
350 + * 字段定义类型
351 + * @typedef {Object} FieldDefinition
352 + * @property {string} label - 字段显示名称
353 + * @property {string} type - 字段类型(见 FIELD_TYPES)
354 + * @property {boolean} required - 是否必填
355 + * @property {string} api_field - API 字段名
356 + * @property {string} [component] - 对应组件名
357 + * @property {string} [placeholder] - 占位符文本
358 + * @property {Array} [options] - 选项列表(select/radio 类型)
359 + * @property {string} [options_from] - 选项来源(从模板配置获取)
360 + * @property {*} [default] - 默认值
361 + * @property {string} [transform] - 值转换类型(见 TRANSFORM_TYPES)
362 + * @property {Object} [validation] - 验证规则
363 + * @property {Function} [validation.required] - 必填验证函数
364 + * @property {string[]} [affects] - 影响的字段列表
365 + * @property {string} [depends_on] - 依赖的字段
366 + * @property {Object} [show_when] - 显示条件
367 + * @property {string} [default_from] - 默认值来源(从其他字段获取)
368 + */
...@@ -34,6 +34,75 @@ ...@@ -34,6 +34,75 @@
34 * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key 34 * form_sn: "life-insurance-wiop3e" // 对应下面的配置 key
35 * } 35 * }
36 */ 36 */
37 +// 基础提交字段映射(适用于人寿/重疾等通用表单)
38 +const baseSubmitMapping = {
39 + customer_name: { api_field: 'customer_name' },
40 + gender: { api_field: 'customer_gender' },
41 + birthday: { api_field: 'customer_birthday' },
42 + smoker: { api_field: 'smoking_status' },
43 + coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' },
44 + payment_period: { api_field: 'payment_years' },
45 + total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' }
46 +}
47 +
48 +// 人寿/重疾基础表单 Schema(通用保障类)
49 +const protectionFormSchema = {
50 + base_fields: [
51 + { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true },
52 + { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true },
53 + { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true },
54 + { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true },
55 + { id: 'coverage', key: 'coverage', type: 'amount', label: '保额', placeholder: '请输入保额', input_label: '请输入保额金额', required: true, currency_from: 'currency' },
56 + { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' }
57 + ]
58 +}
59 +
60 +// 储蓄类提交字段映射(在基础映射上追加提取计划字段)
61 +const savingsSubmitMapping = {
62 + ...baseSubmitMapping,
63 + withdrawal_enabled: { api_field: 'allow_reduce_amount' },
64 + withdrawal_mode: { api_field: 'withdrawal_option' },
65 + withdrawal_method: { api_field: 'withdrawal_method' },
66 + annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
67 + annual_increase_percentage: { api_field: 'annual_increase_percentage' },
68 + withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' },
69 + withdrawal_period_specified: { api_field: 'withdrawal_period' },
70 + withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' },
71 + withdrawal_period_fixed: { api_field: 'withdrawal_period' }
72 +}
73 +
74 +// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口)
75 +const savingsFormSchema = {
76 + // 基础字段:非提取计划部分
77 + base_fields: [
78 + { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true },
79 + { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true },
80 + { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true },
81 + { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true },
82 + { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' },
83 + { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' }
84 + ],
85 + // 提取计划字段:由 withdrawal_plan 开关控制
86 + withdrawal_fields: [
87 + { id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' },
88 + { id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' },
89 + { id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
90 + { id: 'annual_withdrawal_amount', key: 'annual_withdrawal_amount', type: 'amount', label: '每年提取金额', placeholder: '请输入每年提取金额', input_label: '请输入每年提取金额', required: true, currency_from: 'withdrawal_plan.default_currency', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
91 + { id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
92 + { id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
93 + { id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
94 + { id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
95 + { id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }
96 + ],
97 + // 提取模式切换时的清空逻辑,避免脏字段影响提交
98 + reset_map: {
99 + withdrawal_mode: {
100 + '最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'],
101 + '指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed']
102 + }
103 + }
104 +}
105 +
37 export const PLAN_TEMPLATES = { 106 export const PLAN_TEMPLATES = {
38 // 人寿保险产品 - WIOP3E 107 // 人寿保险产品 - WIOP3E
39 'life-insurance-wiop3e': { 108 'life-insurance-wiop3e': {
...@@ -48,7 +117,9 @@ export const PLAN_TEMPLATES = { ...@@ -48,7 +117,9 @@ export const PLAN_TEMPLATES = {
48 '10 年(0-70 岁)' 117 '10 年(0-70 岁)'
49 ], 118 ],
50 age_range: { min: 0, max: 75 }, // 年龄范围 119 age_range: { min: 0, max: 75 }, // 年龄范围
51 - insurance_period: '终身' // 保险期间 120 + insurance_period: '终身', // 保险期间
121 + form_schema: protectionFormSchema,
122 + submit_mapping: baseSubmitMapping
52 } 123 }
53 }, 124 },
54 125
...@@ -64,7 +135,9 @@ export const PLAN_TEMPLATES = { ...@@ -64,7 +135,9 @@ export const PLAN_TEMPLATES = {
64 '10 年(0-70 岁)' 135 '10 年(0-70 岁)'
65 ], 136 ],
66 age_range: { min: 0, max: 75 }, 137 age_range: { min: 0, max: 75 },
67 - insurance_period: '终身' 138 + insurance_period: '终身',
139 + form_schema: protectionFormSchema,
140 + submit_mapping: baseSubmitMapping
68 } 141 }
69 }, 142 },
70 143
...@@ -80,7 +153,9 @@ export const PLAN_TEMPLATES = { ...@@ -80,7 +153,9 @@ export const PLAN_TEMPLATES = {
80 '25 年(15 日 - 60 岁)' 153 '25 年(15 日 - 60 岁)'
81 ], 154 ],
82 age_range: { min: 0, max: 65 }, 155 age_range: { min: 0, max: 65 },
83 - insurance_period: '终身' 156 + insurance_period: '终身',
157 + form_schema: protectionFormSchema,
158 + submit_mapping: baseSubmitMapping
84 } 159 }
85 }, 160 },
86 161
...@@ -96,7 +171,9 @@ export const PLAN_TEMPLATES = { ...@@ -96,7 +171,9 @@ export const PLAN_TEMPLATES = {
96 '25 年(15 日 - 60 岁)' 171 '25 年(15 日 - 60 岁)'
97 ], 172 ],
98 age_range: { min: 0, max: 65 }, 173 age_range: { min: 0, max: 65 },
99 - insurance_period: '终身' 174 + insurance_period: '终身',
175 + form_schema: protectionFormSchema,
176 + submit_mapping: baseSubmitMapping
100 } 177 }
101 }, 178 },
102 179
...@@ -112,7 +189,9 @@ export const PLAN_TEMPLATES = { ...@@ -112,7 +189,9 @@ export const PLAN_TEMPLATES = {
112 '25 年(15 日 - 60 岁)' 189 '25 年(15 日 - 60 岁)'
113 ], 190 ],
114 age_range: { min: 0, max: 65 }, 191 age_range: { min: 0, max: 65 },
115 - insurance_period: '终身' 192 + insurance_period: '终身',
193 + form_schema: protectionFormSchema,
194 + submit_mapping: baseSubmitMapping
116 } 195 }
117 }, 196 },
118 197
...@@ -139,10 +218,7 @@ export const PLAN_TEMPLATES = { ...@@ -139,10 +218,7 @@ export const PLAN_TEMPLATES = {
139 enabled: true, 218 enabled: true,
140 currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 219 currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
141 default_currency: 'USD', // 统一为美元 220 default_currency: 'USD', // 统一为美元
142 - withdrawal_modes: [ 221 + withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
143 - '年龄指定金额', // 方式1
144 - '最高固定金额' // 方式2
145 - ],
146 withdrawal_periods: [ 222 withdrawal_periods: [
147 '1年', 223 '1年',
148 '2年', 224 '2年',
...@@ -153,7 +229,9 @@ export const PLAN_TEMPLATES = { ...@@ -153,7 +229,9 @@ export const PLAN_TEMPLATES = {
153 '20年', 229 '20年',
154 '终身' 230 '终身'
155 ] 231 ]
156 - } 232 + },
233 + form_schema: savingsFormSchema,
234 + submit_mapping: savingsSubmitMapping
157 } 235 }
158 }, 236 },
159 237
...@@ -175,7 +253,7 @@ export const PLAN_TEMPLATES = { ...@@ -175,7 +253,7 @@ export const PLAN_TEMPLATES = {
175 enabled: true, 253 enabled: true,
176 currencies: ['HKD', 'USD', 'CNY'], 254 currencies: ['HKD', 'USD', 'CNY'],
177 default_currency: 'USD', // 统一为美元 255 default_currency: 'USD', // 统一为美元
178 - withdrawal_modes: ['年龄指定金额', '最高固定金额'], 256 + withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
179 withdrawal_periods: [ 257 withdrawal_periods: [
180 '1年', 258 '1年',
181 '2年', 259 '2年',
...@@ -186,7 +264,9 @@ export const PLAN_TEMPLATES = { ...@@ -186,7 +264,9 @@ export const PLAN_TEMPLATES = {
186 '20年', 264 '20年',
187 '终身' 265 '终身'
188 ] 266 ]
189 - } 267 + },
268 + form_schema: savingsFormSchema,
269 + submit_mapping: savingsSubmitMapping
190 } 270 }
191 }, 271 },
192 272
...@@ -208,7 +288,7 @@ export const PLAN_TEMPLATES = { ...@@ -208,7 +288,7 @@ export const PLAN_TEMPLATES = {
208 enabled: true, 288 enabled: true,
209 currencies: ['HKD', 'USD', 'CNY'], 289 currencies: ['HKD', 'USD', 'CNY'],
210 default_currency: 'USD', // 统一为美元 290 default_currency: 'USD', // 统一为美元
211 - withdrawal_modes: ['年龄指定金额', '最高固定金额'], 291 + withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
212 withdrawal_periods: [ 292 withdrawal_periods: [
213 '1年', 293 '1年',
214 '2年', 294 '2年',
...@@ -219,7 +299,9 @@ export const PLAN_TEMPLATES = { ...@@ -219,7 +299,9 @@ export const PLAN_TEMPLATES = {
219 '20年', 299 '20年',
220 '终身' 300 '终身'
221 ] 301 ]
222 - } 302 + },
303 + form_schema: savingsFormSchema,
304 + submit_mapping: savingsSubmitMapping
223 } 305 }
224 }, 306 },
225 307
...@@ -242,7 +324,7 @@ export const PLAN_TEMPLATES = { ...@@ -242,7 +324,7 @@ export const PLAN_TEMPLATES = {
242 enabled: true, 324 enabled: true,
243 currencies: ['HKD', 'USD', 'CNY'], 325 currencies: ['HKD', 'USD', 'CNY'],
244 default_currency: 'USD', // 统一为美元 326 default_currency: 'USD', // 统一为美元
245 - withdrawal_modes: ['年龄指定金额', '最高固定金额'], 327 + withdrawal_modes: ['指定提取金额', '最高固定提取金额'],
246 withdrawal_periods: [ 328 withdrawal_periods: [
247 '1年', 329 '1年',
248 '2年', 330 '2年',
...@@ -253,7 +335,9 @@ export const PLAN_TEMPLATES = { ...@@ -253,7 +335,9 @@ export const PLAN_TEMPLATES = {
253 '20年', 335 '20年',
254 '终身' 336 '终身'
255 ] 337 ]
256 - } 338 + },
339 + form_schema: savingsFormSchema,
340 + submit_mapping: savingsSubmitMapping
257 } 341 }
258 } 342 }
259 } 343 }
......
...@@ -146,12 +146,18 @@ const fetchFavoritesList = async (params = {}, isLoadMore = false) => { ...@@ -146,12 +146,18 @@ const fetchFavoritesList = async (params = {}, isLoadMore = false) => {
146 if (res.code === 1 && res.data && res.data.list) { 146 if (res.code === 1 && res.data && res.data.list) {
147 console.log('[Favorites] 数据:', res.data.list) 147 console.log('[Favorites] 数据:', res.data.list)
148 148
149 + // 处理 name 为 null 的情况,给默认标题"未命名文件"
150 + const processedList = res.data.list.map(item => ({
151 + ...item,
152 + name: item.name || '未命名文件'
153 + }))
154 +
149 if (isLoadMore) { 155 if (isLoadMore) {
150 // 加载更多:追加数据 156 // 加载更多:追加数据
151 - currentList.value = [...currentList.value, ...res.data.list] 157 + currentList.value = [...currentList.value, ...processedList]
152 } else { 158 } else {
153 // 首次加载或刷新:替换数据 159 // 首次加载或刷新:替换数据
154 - currentList.value = res.data.list 160 + currentList.value = processedList
155 } 161 }
156 162
157 // 判断是否还有更多数据 163 // 判断是否还有更多数据
......
...@@ -15,7 +15,9 @@ ...@@ -15,7 +15,9 @@
15 </view> 15 </view>
16 16
17 <!-- Contact Service --> 17 <!-- Contact Service -->
18 + <!-- 通过 features.contactService 控制显示/隐藏 -->
18 <view 19 <view
20 + v-if="features.contactService"
19 class="flex items-center justify-between w-full bg-white rounded-[24rpx] p-[32rpx] mb-[40rpx] shadow-sm relative overflow-hidden" 21 class="flex items-center justify-between w-full bg-white rounded-[24rpx] p-[32rpx] mb-[40rpx] shadow-sm relative overflow-hidden"
20 @tap="showContactPopup = true" 22 @tap="showContactPopup = true"
21 > 23 >
...@@ -129,6 +131,7 @@ import { ref, computed } from 'vue' ...@@ -129,6 +131,7 @@ import { ref, computed } from 'vue'
129 import NavHeader from '@/components/navigation/NavHeader.vue' 131 import NavHeader from '@/components/navigation/NavHeader.vue'
130 import IconFont from '@/components/icons/IconFont.vue' 132 import IconFont from '@/components/icons/IconFont.vue'
131 import SearchBar from '@/components/forms/SearchBar.vue' 133 import SearchBar from '@/components/forms/SearchBar.vue'
134 +import { features } from '@/config/features.js'
132 135
133 // Popup 状态 136 // Popup 状态
134 const showContactPopup = ref(false) 137 const showContactPopup = ref(false)
......
...@@ -95,7 +95,10 @@ ...@@ -95,7 +95,10 @@
95 :tags="product.tags" 95 :tags="product.tags"
96 :class="{ 'mb-[24rpx]': index < hotProducts.length - 1 }" 96 :class="{ 'mb-[24rpx]': index < hotProducts.length - 1 }"
97 @detail="goToProductDetail" 97 @detail="goToProductDetail"
98 - @plan="(productId) => checkPlanPermission(() => openPlanPopup(productId))" 98 + @plan="(productId) => requireLogin(
99 + () => openPlanPopup(productId),
100 + { content: '请先登录后制作专属计划书', confirmText: '立即登录' }
101 + )"
99 /> 102 />
100 </view> 103 </view>
101 </view> 104 </view>
...@@ -173,15 +176,14 @@ import { listAPI } from '@/api/get_product'; ...@@ -173,15 +176,14 @@ import { listAPI } from '@/api/get_product';
173 import { weekHotAPI } from '@/api/file'; 176 import { weekHotAPI } from '@/api/file';
174 import { homeIconAPI } from '@/api/home'; 177 import { homeIconAPI } from '@/api/home';
175 import { usePlanSubmit } from '@/composables/usePlanSubmit'; 178 import { usePlanSubmit } from '@/composables/usePlanSubmit';
176 -import { usePlanPermission } from '@/composables/usePlanPermission'; 179 +import { usePermission } from '@/composables/usePermission';
177 180
181 +// 初始化权限检查
182 +const { requireLogin } = usePermission()
178 183
179 // User Store 184 // User Store
180 const userStore = useUserStore(); 185 const userStore = useUserStore();
181 186
182 -// 获取权限检查方法
183 -const { checkPlanPermission } = usePlanPermission();
184 -
185 // Header Image Error State 187 // Header Image Error State
186 /** 188 /**
187 * 头部图片加载失败状态 189 * 头部图片加载失败状态
...@@ -359,19 +361,15 @@ const fetchHotMaterials = async () => { ...@@ -359,19 +361,15 @@ const fetchHotMaterials = async () => {
359 if (res.code === 1 && res.data && res.data.list) { 361 if (res.code === 1 && res.data && res.data.list) {
360 // 转换 API 数据格式为组件所需格式 362 // 转换 API 数据格式为组件所需格式
361 hotMaterials.value = res.data.list.map(item => { 363 hotMaterials.value = res.data.list.map(item => {
362 - // 提取文件扩展名
363 - const fileName = item.name || '未命名文件'
364 - const extension = item.extension || fileName.split('.').pop()?.toLowerCase() || ''
365 -
366 return { 364 return {
367 id: item.meta_id, 365 id: item.meta_id,
368 title: item.name || '未命名资料', 366 title: item.name || '未命名资料',
369 - fileName: fileName, 367 + fileName: item.name || '未命名文件',
370 downloadUrl: item.src, 368 downloadUrl: item.src,
371 fileSize: item.size, 369 fileSize: item.size,
372 - extension: extension, 370 + // 不在这里提取扩展名,让 MaterialCard 内部使用 extractExtensionFromFile 自动从 URL 解析
373 learners: `${item.read_people_count}人学习`, 371 learners: `${item.read_people_count}人学习`,
374 - readPeoplePercent: item.read_people_percent, // 学习人数比例 372 + readPeoplePercent: item.read_people_percent,
375 collected: item.is_favorite 373 collected: item.is_favorite
376 } 374 }
377 }); 375 });
...@@ -431,16 +429,28 @@ const handleGridNav = (item) => { ...@@ -431,16 +429,28 @@ const handleGridNav = (item) => {
431 delete params.name; 429 delete params.name;
432 delete params.route; 430 delete params.route;
433 431
434 - // 如果有参数(如 cid),则带参数跳转 432 + // 定义导航执行函数
433 + const navigate = () => {
435 if (Object.keys(params).length > 0) { 434 if (Object.keys(params).length > 0) {
436 go(item.route, { 435 go(item.route, {
437 ...params, 436 ...params,
438 title: item.name // 将导航名称作为页面标题 437 title: item.name // 将导航名称作为页面标题
439 }); 438 });
440 } else { 439 } else {
441 - // 无参数,直接跳转
442 go(item.route); 440 go(item.route);
443 } 441 }
442 + };
443 +
444 + // 特殊处理:计划书页面需要登录权限
445 + if (item.route === '/pages/plan/index') {
446 + requireLogin(
447 + () => navigate(),
448 + { content: '请先登录后查看专属计划书', confirmText: '立即登录' }
449 + );
450 + } else {
451 + // 其他页面直接导航
452 + navigate();
453 + }
444 }; 454 };
445 455
446 // 跳转到产品详情页 456 // 跳转到产品详情页
......
...@@ -173,13 +173,16 @@ const getProposalStatusText = (status) => { ...@@ -173,13 +173,16 @@ const getProposalStatusText = (status) => {
173 } 173 }
174 174
175 /** 175 /**
176 - * 格式化富文本内容,处理图片宽度等问题 176 + * 格式化富文本内容,处理图片宽度、文本换行等问题
177 */ 177 */
178 const formattedContent = computed(() => { 178 const formattedContent = computed(() => {
179 if (!detail.value?.note) return '' 179 if (!detail.value?.note) return ''
180 180
181 - // 简单的正则替换,确保图片宽度不超过容器 181 + // 1. 处理文本换行:将真正的换行符替换为 <br> 标签
182 - const content = detail.value.note.replace( 182 + let content = detail.value.note.replace(/\n/g, '<br>')
183 +
184 + // 2. 处理图片样式:确保图片宽度不超过容器
185 + content = content.replace(
183 /<img/g, 186 /<img/g,
184 '<img style="max-width:100%;height:auto;display:block;border-radius:8px;margin:10px 0;"' 187 '<img style="max-width:100%;height:auto;display:block;border-radius:8px;margin:10px 0;"'
185 ) 188 )
......
1 <!-- 1 <!--
2 * @Date:2026-02-08 2 * @Date:2026-02-08
3 * @Description: 我的消息页 - 使用 LoadMoreList 组件重构版本 3 * @Description: 我的消息页 - 使用 LoadMoreList 组件重构版本
4 - * @Update:2026-02-13 API 新增 title 字段,直接使用 API 返回的标题 4 + * @Update:2026-02-14 简化逻辑:只使用 title 字段,移除 note 相关处理
5 --> 5 -->
6 <template> 6 <template>
7 <LoadMoreList 7 <LoadMoreList
...@@ -29,17 +29,19 @@ ...@@ -29,17 +29,19 @@
29 @tap="handleItemClick(item)" 29 @tap="handleItemClick(item)"
30 > 30 >
31 <!-- 顶部:标题与红点 --> 31 <!-- 顶部:标题与红点 -->
32 - <view class="flex justify-between items-start mb-2"> 32 + <view class="mb-2">
33 - <view class="flex-1 mr-2 relative"> 33 + <view class="flex-1 relative">
34 <!-- 未读红点 --> 34 <!-- 未读红点 -->
35 <view v-if="item.status === 'send'" class="absolute -left-2 top-1.5 w-1.5 h-1.5 bg-red-500 rounded-full"></view> 35 <view v-if="item.status === 'send'" class="absolute -left-2 top-1.5 w-1.5 h-1.5 bg-red-500 rounded-full"></view>
36 - <!-- 标题:优先使用 API 返回的 title,降级使用 note 第一行 --> 36 + <!-- 标题:使用 API 返回的 title -->
37 - <text class="text-lg font-bold text-gray-900 line-clamp-1 leading-snug"> 37 + <text class="text-base font-bold text-gray-900 leading-snug text-justify">
38 - {{ item.title || getItemTitle(item.note) }} 38 + {{ item.title || '暂无标题' }}
39 </text> 39 </text>
40 </view> 40 </view>
41 + </view>
41 42
42 - <!-- 状态标签 --> 43 + <!-- 状态标签行:靠右对齐 -->
44 + <view class="flex justify-end mb-2">
43 <view v-if="item.status === 'send'" class="shrink-0 px-2 py-1 bg-red-50 text-red-600 rounded text-xs font-medium border border-red-100"> 45 <view v-if="item.status === 'send'" class="shrink-0 px-2 py-1 bg-red-50 text-red-600 rounded text-xs font-medium border border-red-100">
44 未读 46 未读
45 </view> 47 </view>
...@@ -50,8 +52,8 @@ ...@@ -50,8 +52,8 @@
50 52
51 <!-- 中间:内容预览 --> 53 <!-- 中间:内容预览 -->
52 <view class="mb-4"> 54 <view class="mb-4">
53 - <text class="text-sm text-gray-500 line-clamp-2 leading-relaxed"> 55 + <text class="text-xs text-gray-500 leading-relaxed">
54 - {{ getItemPreview(item.note) }} 56 + {{ item.note ? '点击查看详情' : '暂无内容' }}
55 </text> 57 </text>
56 </view> 58 </view>
57 59
...@@ -100,56 +102,6 @@ const loadingMore = ref(false) ...@@ -100,56 +102,6 @@ const loadingMore = ref(false)
100 // 标记:是否首次加载(用于区分 useLoad 和 useDidShow) 102 // 标记:是否首次加载(用于区分 useLoad 和 useDidShow)
101 const isFirstLoad = ref(true) 103 const isFirstLoad = ref(true)
102 104
103 -/**
104 - * 提取消息标题(降级方案:从 note 第一行提取)
105 - *
106 - * @description 当 API 未返回 title 时,从 note 内容的第一行提取标题
107 - * @param {string} note - 消息内容
108 - * @returns {string} 标题
109 - *
110 - * @example
111 - * // API 已返回 title
112 - * getItemTitle(note) // 不使用,直接显示 item.title
113 - * // API 未返回 title(降级)
114 - * getItemTitle('这是第一行标题\n这是内容') // 返回: '这是第一行标题'
115 - */
116 -const getItemTitle = (note) => {
117 - if (!note) return '暂无消息内容'
118 -
119 - // 提取第一行作为标题
120 - const firstLine = note.split('\n')[0]
121 -
122 - // 移除富文本标签(简单处理)
123 - const textOnly = firstLine.replace(/<[^>]+>/g, '').trim()
124 -
125 - // 如果第一行太长,截取前 50 个字符
126 - return textOnly.length > 50 ? textOnly.substring(0, 50) + '...' : textOnly
127 -}
128 -
129 -/**
130 - * 提取消息预览
131 - *
132 - * @description 移除第一行标题后的内容作为预览
133 - * @param {string} note - 消息内容
134 - * @returns {string} 预览内容
135 - *
136 - * @example
137 - * getItemPreview('标题\n内容第二行\n内容第三行') // 返回: '内容第二行\n内容第三行'
138 - * getItemPreview('只有单行内容') // 返回: '点击查看详情'
139 - */
140 -const getItemPreview = (note) => {
141 - if (!note) return '点击查看详情'
142 -
143 - // 移除第一行(已作为标题显示)
144 - const lines = note.split('\n')
145 - if (lines.length > 1) {
146 - // 移除富文本标签(简单处理)
147 - const preview = lines.slice(1).join('\n').replace(/<[^>]+>/g, '').trim()
148 - return preview || '点击查看详情'
149 - }
150 -
151 - return '点击查看详情' // 只有一行时
152 -}
153 105
154 /** 106 /**
155 * 获取消息列表 107 * 获取消息列表
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
52 <!-- Menu List --> 52 <!-- Menu List -->
53 <!-- Added subtle styling to icons and softened the container --> 53 <!-- Added subtle styling to icons and softened the container -->
54 <view class="bg-white w-full rounded-[32rpx] shadow-sm mb-[40rpx] p-[16rpx]"> 54 <view class="bg-white w-full rounded-[32rpx] shadow-sm mb-[40rpx] p-[16rpx]">
55 - <view v-for="(item, index) in menuItems" :key="index" 55 + <view v-for="item in menuItems" :key="item.key"
56 class="flex items-center justify-between p-[24rpx] rounded-[20rpx] active:bg-gray-50 transition-all duration-200" 56 class="flex items-center justify-between p-[24rpx] rounded-[20rpx] active:bg-gray-50 transition-all duration-200"
57 @tap="handleMenuClick(item)"> 57 @tap="handleMenuClick(item)">
58 <view class="flex items-center"> 58 <view class="flex items-center">
...@@ -98,6 +98,7 @@ import { useUserStore } from '@/stores/user' ...@@ -98,6 +98,7 @@ import { useUserStore } from '@/stores/user'
98 import IconFont from '@/components/icons/IconFont.vue' 98 import IconFont from '@/components/icons/IconFont.vue'
99 import TabBar from '@/components/navigation/TabBar.vue' 99 import TabBar from '@/components/navigation/TabBar.vue'
100 import NavHeader from '@/components/navigation/NavHeader.vue' 100 import NavHeader from '@/components/navigation/NavHeader.vue'
101 +import { features } from '@/config/features.js'
101 import Taro, { useLoad, useDidShow } from '@tarojs/taro' 102 import Taro, { useLoad, useDidShow } from '@tarojs/taro'
102 import defaultAvatar from '@/assets/images/icon/avatar.svg' 103 import defaultAvatar from '@/assets/images/icon/avatar.svg'
103 104
...@@ -137,7 +138,7 @@ useDidShow(() => { ...@@ -137,7 +138,7 @@ useDidShow(() => {
137 138
138 // Modern Professional Palette 139 // Modern Professional Palette
139 // Using subtle background colors for icons to add vitality without being "playful" 140 // Using subtle background colors for icons to add vitality without being "playful"
140 -const menuItems = [ 141 +const rawMenuItems = [
141 { 142 {
142 key: 'plan', 143 key: 'plan',
143 title: '我的计划书', 144 title: '我的计划书',
...@@ -180,6 +181,17 @@ const menuItems = [ ...@@ -180,6 +181,17 @@ const menuItems = [
180 } 181 }
181 ] 182 ]
182 183
184 +// 根据功能配置过滤菜单项
185 +const menuItems = computed(() => {
186 + return rawMenuItems.filter(item => {
187 + // 如果配置了 feedback 关闭,过滤掉意见反馈菜单
188 + if (item.key === 'feedback' && !features.feedback) {
189 + return false
190 + }
191 + return true
192 + })
193 +})
194 +
183 const handleMenuClick = (item) => { 195 const handleMenuClick = (item) => {
184 if (item.path) { 196 if (item.path) {
185 go(item.path) 197 go(item.path)
......
...@@ -151,7 +151,11 @@ import PlanFormContainer from '@/components/plan/PlanFormContainer.vue' ...@@ -151,7 +151,11 @@ import PlanFormContainer from '@/components/plan/PlanFormContainer.vue'
151 import { listAPI } from '@/api/get_product' 151 import { listAPI } from '@/api/get_product'
152 import { mockProductListAPI } from '@/utils/mockData' 152 import { mockProductListAPI } from '@/utils/mockData'
153 import { usePlanSubmit } from '@/composables/usePlanSubmit' 153 import { usePlanSubmit } from '@/composables/usePlanSubmit'
154 -import { usePlanPermission } from '@/composables/usePlanPermission' 154 +import { usePermission } from '@/composables/usePermission'
155 +
156 +// 初始化权限检查
157 +const { requireLogin } = usePermission()
158 +
155 import { USE_MOCK_DATA } from '@/config/app' 159 import { USE_MOCK_DATA } from '@/config/app'
156 // ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入 160 // ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入
157 161
...@@ -160,9 +164,6 @@ const go = useGo() ...@@ -160,9 +164,6 @@ const go = useGo()
160 // User Store 164 // User Store
161 const userStore = useUserStore() 165 const userStore = useUserStore()
162 166
163 -// 获取权限检查方法
164 -const { checkPlanPermission } = usePlanPermission()
165 -
166 /** 167 /**
167 * 当前列表数据 168 * 当前列表数据
168 * @type {Ref<Array<any>>} 169 * @type {Ref<Array<any>>}
...@@ -502,11 +503,14 @@ const { handleClick: handleProductClick } = useListItemClick({ ...@@ -502,11 +503,14 @@ const { handleClick: handleProductClick } = useListItemClick({
502 */ 503 */
503 const openPlanPopup = (product) => { 504 const openPlanPopup = (product) => {
504 console.log('[Product Center] 点击制作计划书,当前登录状态:', userStore.isLoggedIn) 505 console.log('[Product Center] 点击制作计划书,当前登录状态:', userStore.isLoggedIn)
505 - checkPlanPermission(() => { 506 + requireLogin(
507 + () => {
506 console.log('[Product Center] 权限检查通过,打开计划书弹窗') 508 console.log('[Product Center] 权限检查通过,打开计划书弹窗')
507 selectedProduct.value = product 509 selectedProduct.value = product
508 showPlanPopup.value = true 510 showPlanPopup.value = true
509 - }) 511 + },
512 + { content: '请先登录后制作专属计划书', confirmText: '立即登录' }
513 + )
510 } 514 }
511 515
512 // 使用 composable 统一处理计划书提交后逻辑 516 // 使用 composable 统一处理计划书提交后逻辑
......
...@@ -104,7 +104,10 @@ ...@@ -104,7 +104,10 @@
104 <nut-button 104 <nut-button
105 color="#2563EB" 105 color="#2563EB"
106 class="!w-full !h-[88rpx] !rounded-[16rpx] !text-[28rpx] !font-bold" 106 class="!w-full !h-[88rpx] !rounded-[16rpx] !text-[28rpx] !font-bold"
107 - @tap="() => checkPlanPermission(() => openPlanPopup())" 107 + @tap="() => requireLogin(
108 + () => openPlanPopup(),
109 + { content: '请先登录后制作专属计划书', confirmText: '立即登录' }
110 + )"
108 > 111 >
109 制作计划书 112 制作计划书
110 </nut-button> 113 </nut-button>
...@@ -134,7 +137,10 @@ import Taro, { useLoad } from '@tarojs/taro' ...@@ -134,7 +137,10 @@ import Taro, { useLoad } from '@tarojs/taro'
134 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' 137 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
135 import { detailAPI } from '@/api/get_product' 138 import { detailAPI } from '@/api/get_product'
136 import { usePlanSubmit } from '@/composables/usePlanSubmit' 139 import { usePlanSubmit } from '@/composables/usePlanSubmit'
137 -import { usePlanPermission } from '@/composables/usePlanPermission' 140 +import { usePermission } from '@/composables/usePermission'
141 +
142 +// 初始化权限检查
143 +const { requireLogin } = usePermission()
138 144
139 const { viewFile } = useFileOperation() 145 const { viewFile } = useFileOperation()
140 146
...@@ -214,7 +220,6 @@ const viewDocument = (doc) => { ...@@ -214,7 +220,6 @@ const viewDocument = (doc) => {
214 * 220 *
215 * @description 检查登录权限后,打开当前产品的计划书表单 221 * @description 检查登录权限后,打开当前产品的计划书表单
216 */ 222 */
217 -const { checkPlanPermission } = usePlanPermission()
218 223
219 const openPlanPopup = () => { 224 const openPlanPopup = () => {
220 showPlanPopup.value = true 225 showPlanPopup.value = true
......
...@@ -14,6 +14,7 @@ import { searchAPI } from '@/api/search' ...@@ -14,6 +14,7 @@ import { searchAPI } from '@/api/search'
14 vi.mock('@tarojs/taro', () => ({ 14 vi.mock('@tarojs/taro', () => ({
15 default: { 15 default: {
16 showToast: vi.fn(), 16 showToast: vi.fn(),
17 + showModal: vi.fn(),
17 getCurrentPages: vi.fn(() => []) 18 getCurrentPages: vi.fn(() => [])
18 }, 19 },
19 useDidShow: vi.fn(), 20 useDidShow: vi.fn(),
......
...@@ -74,7 +74,10 @@ ...@@ -74,7 +74,10 @@
74 :tags="item.tags || []" 74 :tags="item.tags || []"
75 class="search-result-item" 75 class="search-result-item"
76 @detail="goToProductDetail" 76 @detail="goToProductDetail"
77 - @plan="(productId) => checkPlanPermission(() => openPlanPopup(productId))" 77 + @plan="(productId) => requireLogin(
78 + () => openPlanPopup(productId),
79 + { content: '请先登录后制作专属计划书', confirmText: '立即登录' }
80 + )"
78 /> 81 />
79 82
80 <!-- File Results --> 83 <!-- File Results -->
...@@ -140,7 +143,6 @@ import { mockSearchAPI } from '@/utils/mockData' ...@@ -140,7 +143,6 @@ import { mockSearchAPI } from '@/utils/mockData'
140 import { USE_MOCK_DATA } from '@/config/app' 143 import { USE_MOCK_DATA } from '@/config/app'
141 import { usePlanSubmit } from '@/composables/usePlanSubmit' 144 import { usePlanSubmit } from '@/composables/usePlanSubmit'
142 import { usePermission } from '@/composables/usePermission' 145 import { usePermission } from '@/composables/usePermission'
143 -import { usePlanPermission } from '@/composables/usePlanPermission'
144 146
145 // ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入 147 // ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入
146 148
...@@ -165,9 +167,6 @@ onMounted(() => { ...@@ -165,9 +167,6 @@ onMounted(() => {
165 ) 167 )
166 }) 168 })
167 169
168 -// 获取权限检查方法
169 -const { checkPlanPermission } = usePlanPermission()
170 -
171 /** 170 /**
172 * 搜索页面状态管理 171 * 搜索页面状态管理
173 * @description 支持双类型(产品/资料)搜索,自动切换分类 172 * @description 支持双类型(产品/资料)搜索,自动切换分类
...@@ -276,17 +275,13 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo ...@@ -276,17 +275,13 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
276 275
277 // 映射资料列表(进行字段映射,与首页保持一致) 276 // 映射资料列表(进行字段映射,与首页保持一致)
278 const newFiles = (res.data.files.list || []).map(item => { 277 const newFiles = (res.data.files.list || []).map(item => {
279 - // 提取文件扩展名
280 - const fileName = item.name || '未命名文件'
281 - const extension = item.extension || fileName.split('.').pop()?.toLowerCase() || ''
282 -
283 return { 278 return {
284 id: item.meta_id || item.id, 279 id: item.meta_id || item.id,
285 title: item.name, 280 title: item.name,
286 - fileName: fileName, 281 + fileName: item.name || '未命名文件',
287 fileSize: item.size || item.file_size, 282 fileSize: item.size || item.file_size,
288 downloadUrl: item.src || item.value, 283 downloadUrl: item.src || item.value,
289 - extension: extension, 284 + // 不手动提取 extension,让 MaterialCard 内部使用 extractExtensionFromFile 自动从 URL 解析
290 learners: item.read_people_count ? `${item.read_people_count }人学习` : '', 285 learners: item.read_people_count ? `${item.read_people_count }人学习` : '',
291 readPeoplePercent: item.read_people_percent, 286 readPeoplePercent: item.read_people_percent,
292 is_favorite: item.is_favorite, // 保留原始字段 287 is_favorite: item.is_favorite, // 保留原始字段
......
...@@ -134,15 +134,13 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => { ...@@ -134,15 +134,13 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
134 // 处理列表数据 134 // 处理列表数据
135 if (res.data.list?.length) { 135 if (res.data.list?.length) {
136 // 直接映射为 MaterialCard 需要的格式 136 // 直接映射为 MaterialCard 需要的格式
137 - // 注意:extension 字段可以让 extractExtensionFromFile 函数自动从 URL 解析 137 + // 不手动提取 extension,让 MaterialCard 内部使用 extractExtensionFromFile 自动从 URL 解析
138 const listData = res.data.list.map(item => { 138 const listData = res.data.list.map(item => {
139 return { 139 return {
140 meta_id: item.meta_id, 140 meta_id: item.meta_id,
141 name: item.name || '未命名文件', 141 name: item.name || '未命名文件',
142 size: item.size || '', 142 size: item.size || '',
143 downloadUrl: item.src, 143 downloadUrl: item.src,
144 - // extension 可以为空,MaterialCard 会使用 extractExtensionFromFile 从 src 解析
145 - extension: item.extension || '',
146 collected: item.is_favorite === '1' || item.is_favorite === 1 || item.is_favorite === true, 144 collected: item.is_favorite === '1' || item.is_favorite === 1 || item.is_favorite === true,
147 read_people_count: item.read_people_count, 145 read_people_count: item.read_people_count,
148 read_people_percent: item.read_people_percent 146 read_people_percent: item.read_people_percent
......
1 +/**
2 + * planFieldTransformers 单元测试
3 + * @description 测试字段值转换工具函数
4 + * @module utils/__tests__/planFieldTransformers.test
5 + */
6 +
7 +import { describe, it, expect } from 'vitest'
8 +import {
9 + fenToYuan,
10 + yuanToFen,
11 + formatAge,
12 + noneTransform,
13 + transformFieldValue,
14 + batchTransformFields,
15 + reverseTransformFields
16 +} from '../planFieldTransformers'
17 +import { TRANSFORM_TYPES } from '@/config/plan-fields'
18 +
19 +describe('fenToYuan', () => {
20 + it('should convert fen to yuan correctly', () => {
21 + expect(fenToYuan(10000)).toBe('100.00')
22 + expect(fenToYuan(100)).toBe('1.00')
23 + expect(fenToYuan(1)).toBe('0.01')
24 + })
25 +
26 + it('should handle zero', () => {
27 + expect(fenToYuan(0)).toBe('0.00')
28 + })
29 +
30 + it('should handle null and undefined', () => {
31 + expect(fenToYuan(null)).toBe(null)
32 + expect(fenToYuan(undefined)).toBe(undefined) // 保持原值
33 + })
34 +
35 + it('should handle string numbers', () => {
36 + expect(fenToYuan('10000')).toBe('100.00')
37 + expect(fenToYuan('100')).toBe('1.00')
38 + })
39 +
40 + it('should handle invalid values', () => {
41 + expect(fenToYuan('invalid')).toBe(null)
42 + expect(fenToYuan(NaN)).toBe(null)
43 + })
44 +})
45 +
46 +describe('yuanToFen', () => {
47 + it('should convert yuan to fen correctly', () => {
48 + expect(yuanToFen('100.00')).toBe(10000)
49 + expect(yuanToFen('1.00')).toBe(100)
50 + expect(yuanToFen('0.01')).toBe(1)
51 + })
52 +
53 + it('should handle zero', () => {
54 + expect(yuanToFen('0.00')).toBe(0)
55 + expect(yuanToFen(0)).toBe(0)
56 + })
57 +
58 + it('should handle null and undefined', () => {
59 + expect(yuanToFen(null)).toBe(null)
60 + expect(yuanToFen(undefined)).toBe(null)
61 + })
62 +
63 + it('should handle string numbers', () => {
64 + expect(yuanToFen('100.00')).toBe(10000)
65 + expect(yuanToFen('100')).toBe(10000)
66 + })
67 +})
68 +
69 +describe('formatAge', () => {
70 + it('should format age correctly', () => {
71 + expect(formatAge(25)).toBe('25岁')
72 + expect(formatAge(0)).toBe('0岁')
73 + })
74 +
75 + it('should handle null and undefined', () => {
76 + expect(formatAge(null)).toBe(null)
77 + expect(formatAge(undefined)).toBe(null)
78 + })
79 +})
80 +
81 +describe('transformFieldValue', () => {
82 + it('should transform fen to yuan', () => {
83 + expect(transformFieldValue(10000, TRANSFORM_TYPES.FEN_TO_YUAN)).toBe('100.00')
84 + })
85 +
86 + it('should transform yuan to fen', () => {
87 + expect(transformFieldValue('100.00', TRANSFORM_TYPES.YUAN_TO_FEN)).toBe(10000)
88 + })
89 +
90 + it('should pass through for none transform', () => {
91 + expect(transformFieldValue('test', TRANSFORM_TYPES.NONE)).toBe('test')
92 + })
93 +
94 + it('should handle null values', () => {
95 + expect(transformFieldValue(null, TRANSFORM_TYPES.FEN_TO_YUAN)).toBe(null)
96 + })
97 +})
98 +
99 +describe('batchTransformFields', () => {
100 + it('should transform multiple fields according to definitions', () => {
101 + const formData = {
102 + coverage: 10000,
103 + annual_withdrawal_amount: 5000,
104 + name: 'Test'
105 + }
106 +
107 + const fieldDefinitions = {
108 + coverage: {
109 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
110 + },
111 + annual_withdrawal_amount: {
112 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
113 + },
114 + name: {
115 + // 无 transform 属性
116 + }
117 + }
118 +
119 + const result = batchTransformFields(formData, fieldDefinitions)
120 +
121 + expect(result.coverage).toBe('100.00')
122 + expect(result.annual_withdrawal_amount).toBe('50.00')
123 + expect(result.name).toBe('Test') // 保持原值
124 + })
125 +
126 + it('should skip undefined values', () => {
127 + const formData = {
128 + coverage: undefined,
129 + name: 'Test'
130 + }
131 +
132 + const fieldDefinitions = {
133 + coverage: {
134 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
135 + },
136 + name: {}
137 + }
138 +
139 + const result = batchTransformFields(formData, fieldDefinitions)
140 +
141 + // coverage 是 undefined,转换后应该仍然是 undefined
142 + expect(result.coverage).toBeUndefined()
143 + // name 没有变化,应该保持原值
144 + expect(result.name).toBe('Test')
145 + })
146 +})
147 +
148 +describe('reverseTransformFields', () => {
149 + it('should convert API data back to form format', () => {
150 + const apiData = {
151 + annual_premium: '100.00',
152 + annual_withdrawal_amount: '50.00',
153 + name: 'Test'
154 + }
155 +
156 + const fieldDefinitions = {
157 + annual_premium: {
158 + api_field: 'annual_premium',
159 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
160 + },
161 + annual_withdrawal_amount: {
162 + api_field: 'annual_withdrawal_amount',
163 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
164 + },
165 + name: {
166 + api_field: 'name'
167 + }
168 + }
169 +
170 + const result = reverseTransformFields(apiData, fieldDefinitions)
171 +
172 + // formKey 是 annual_premium,所以结果键是 annual_premium
173 + // 反向转换:yuan -> fen,返回整数(分)
174 + expect(result.annual_premium).toBe(10000)
175 + expect(result.annual_withdrawal_amount).toBe(5000)
176 + expect(result.name).toBe('Test')
177 + })
178 +
179 + it('should handle missing fields', () => {
180 + const apiData = {
181 + annual_premium: '100.00'
182 + }
183 +
184 + const fieldDefinitions = {
185 + annual_premium: {
186 + api_field: 'annual_premium',
187 + transform: TRANSFORM_TYPES.FEN_TO_YUAN
188 + },
189 + name: {
190 + api_field: 'name'
191 + }
192 + }
193 +
194 + const result = reverseTransformFields(apiData, fieldDefinitions)
195 +
196 + // annual_premium 反向转换:yuan -> fen,返回整数(分)
197 + expect(result.annual_premium).toBe(10000)
198 + // name 在 apiData 中不存在,所以 result.name 是 undefined
199 + expect(result.name).toBeUndefined()
200 + })
201 +})
1 +/**
2 + * planFieldValidation 单元测试
3 + *
4 + * @description 测试字段验证系统
5 + * @module utils/__tests__/planFieldValidation.test
6 + */
7 +
8 +import { describe, it, expect } from 'vitest'
9 +import { validateField, validateForm, VALIDATION_RULES } from '../planFieldValidation'
10 +import { isNotEmpty } from '../planFieldValidation'
11 +
12 +describe('validateField', () => {
13 + it('should pass required validation for non-empty string', () => {
14 + const result = validateField('John', { required: true })
15 + expect(result.valid).toBe(true)
16 + })
17 +
18 + it('should fail required validation for empty string', () => {
19 + const result = validateField('', { required: true })
20 + expect(result.valid).toBe(false)
21 + expect(result.error).toContain('该字段为必填')
22 + })
23 +
24 + it('should pass required validation for whitespace string', () => {
25 + const result = validateField(' ', { required: true })
26 + expect(result.valid).toBe(false)
27 + })
28 +
29 + it('should pass min validation', () => {
30 + const result = validateField('abc', { min: 3 })
31 + expect(result.valid).toBe(true)
32 + })
33 +
34 + it('should fail min validation', () => {
35 + const result = validateField('ab', { min: 3 })
36 + expect(result.valid).toBe(false)
37 + expect(result.error).toContain('至少需要3个字符')
38 + })
39 +
40 + it('should pass max validation', () => {
41 + const result = validateField('abcde', { max: 5 })
42 + expect(result.valid).toBe(true)
43 + })
44 +
45 + it('should fail max validation', () => {
46 + const result = validateField('abcdef', { max: 5 })
47 + expect(result.valid).toBe(false)
48 + expect(result.error).toContain('最多5个字符')
49 + })
50 +
51 + it('should pass range validation', () => {
52 + const result = validateField(18, { range: [18, 65] })
53 + expect(result.valid).toBe(true)
54 + })
55 +
56 + it('should fail range validation (too small)', () => {
57 + const result = validateField(17, { range: [18, 65] })
58 + expect(result.valid).toBe(false)
59 + expect(result.error).toContain('18-65')
60 + })
61 +
62 + it('should fail range validation (too large)', () => {
63 + const result = validateField(66, { range: [18, 65] })
64 + expect(result.valid).toBe(false)
65 + expect(result.error).toContain('18-65')
66 + })
67 +
68 + it('should pass pattern validation', () => {
69 + const result = validateField('13800138000', { pattern: '^1[3-9]\\d{9}$' })
70 + expect(result.valid).toBe(true)
71 + })
72 +
73 + it('should fail pattern validation', () => {
74 + const result = validateField('123456', { pattern: '^1[3-9]\\d{9}$' })
75 + expect(result.valid).toBe(false)
76 + expect(result.error).toContain('格式不正确')
77 + })
78 +
79 + it('should pass custom validation', () => {
80 + const result = validateField(25, {
81 + custom: (value) => value >= 18
82 + })
83 + expect(result.valid).toBe(true)
84 + })
85 +
86 + it('should fail custom validation', () => {
87 + const result = validateField(15, {
88 + custom: (value) => value >= 18
89 + })
90 + expect(result.valid).toBe(false)
91 + expect(result.error).toBe('验证失败')
92 + })
93 +
94 + it('should handle null value', () => {
95 + const result = validateField(null, { required: true })
96 + expect(result.valid).toBe(false)
97 + expect(result.error).toContain('该字段为必填')
98 + })
99 +
100 + it('should handle undefined value', () => {
101 + const result = validateField(undefined, { required: true })
102 + expect(result.valid).toBe(false)
103 + expect(result.error).toContain('该字段为必填')
104 + })
105 +})
106 +
107 +describe('validateForm', () => {
108 + it('should pass with valid data', () => {
109 + const formData = {
110 + customer_name: '张三',
111 + gender: 'male',
112 + age: 25
113 + }
114 +
115 + const fieldDefinitions = {
116 + customer_name: {
117 + validation: { required: (value) => value?.trim()?.length >= 2 }
118 + },
119 + age: {
120 + validation: { min: (value, ctx) => value >= ctx.age_range.min }
121 + }
122 + }
123 +
124 + const result = validateForm(formData, fieldDefinitions)
125 + expect(result.valid).toBe(true)
126 + expect(result.errors).toEqual({})
127 + })
128 +
129 + it('should fail with invalid data', () => {
130 + const formData = {
131 + customer_name: '张',
132 + age: 25
133 + }
134 +
135 + const fieldDefinitions = {
136 + customer_name: {
137 + validation: {
138 + required: (value) => value?.trim()?.length >= 2
139 + }
140 + },
141 + age: {
142 + validation: { min: (value) => value >= 18 }
143 + }
144 + }
145 +
146 + const result = validateForm(formData, fieldDefinitions, { age_range: { min: 18 } })
147 + expect(result.valid).toBe(false)
148 + expect(Object.keys(result.errors).length).toBeGreaterThan(0)
149 + })
150 +
151 + it('should skip validation for null/undefined values', () => {
152 + const formData = {
153 + customer_name: null,
154 + age: undefined
155 + }
156 +
157 + const fieldDefinitions = {
158 + customer_name: {
159 + validation: { required: (value) => value?.trim()?.length >= 2 }
160 + }
161 + }
162 +
163 + const result = validateForm(formData, fieldDefinitions)
164 + expect(result.valid).toBe(true)
165 + })
166 +
167 + it('should support context-dependent validation', () => {
168 + const formData = { age: 25 }
169 +
170 + const fieldDefinitions = {
171 + age: {
172 + validation: {
173 + min: (value, ctx) => value >= ctx.min_age,
174 + max: (value, ctx) => value <= ctx.max_age
175 + }
176 + }
177 + }
178 +
179 + const result = validateForm(formData, fieldDefinitions, {
180 + min_age: 18,
181 + max_age: 60
182 + })
183 + expect(result.valid).toBe(true)
184 + })
185 +})
186 +
187 +describe('isNotEmpty', () => {
188 + it('should return true for non-empty string', () => {
189 + expect(isNotEmpty('test')).toBe(true)
190 + })
191 +
192 + it('should return false for empty string', () => {
193 + expect(isNotEmpty('')).toBe(false)
194 + })
195 +
196 + it('should return false for null', () => {
197 + expect(isNotEmpty(null)).toBe(false)
198 + })
199 +
200 + it('should return false for undefined', () => {
201 + expect(isNotEmpty(undefined)).toBe(false)
202 + })
203 +
204 + it('should return false for whitespace string', () => {
205 + expect(isNotEmpty(' ')).toBe(false)
206 + })
207 +
208 + it('should return false for empty array', () => {
209 + expect(isNotEmpty([])).toBe(false)
210 + })
211 +})
...@@ -138,6 +138,7 @@ export function getDocumentIcon(fileNameOrItem) { ...@@ -138,6 +138,7 @@ export function getDocumentIcon(fileNameOrItem) {
138 * 从文件名或文件对象中提取扩展名(统一工具函数) 138 * 从文件名或文件对象中提取扩展名(统一工具函数)
139 * 139 *
140 * @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段。 140 * @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段。
141 + * 如果 extension 为空,会依次从 fileName、src、downloadUrl 解析,直到找到扩展名。
141 * 这是项目中所有文件类型判断的核心工具函数。 142 * 这是项目中所有文件类型判断的核心工具函数。
142 * @param {string|Object} fileNameOrItem - 文件名(如:document.pdf)或文件对象(包含 extension 字段) 143 * @param {string|Object} fileNameOrItem - 文件名(如:document.pdf)或文件对象(包含 extension 字段)
143 * @param {string} [fileNameOrItem.fileName] - 文件名 144 * @param {string} [fileNameOrItem.fileName] - 文件名
...@@ -152,6 +153,7 @@ export function getDocumentIcon(fileNameOrItem) { ...@@ -152,6 +153,7 @@ export function getDocumentIcon(fileNameOrItem) {
152 * extractExtensionFromFile({ fileName: 'document.DOC' }) // 'doc'(从文件名解析) 153 * extractExtensionFromFile({ fileName: 'document.DOC' }) // 'doc'(从文件名解析)
153 * extractExtensionFromFile({ extension: 'pdf', fileName: 'backup.doc' }) // 'pdf'(优先使用 extension) 154 * extractExtensionFromFile({ extension: 'pdf', fileName: 'backup.doc' }) // 'pdf'(优先使用 extension)
154 * extractExtensionFromFile({ src: 'https://example.com/file.png' }) // 'png'(从 URL 解析) 155 * extractExtensionFromFile({ src: 'https://example.com/file.png' }) // 'png'(从 URL 解析)
156 + * extractExtensionFromFile({ fileName: '无扩展名文件', src: 'https://cdn.com/file.jpeg' }) // 'jpeg'(src 补位)
155 * extractExtensionFromFile({ downloadUrl: 'https://cdn.com/file.jpg' }) // 'jpg'(从 URL 解析) 157 * extractExtensionFromFile({ downloadUrl: 'https://cdn.com/file.jpg' }) // 'jpg'(从 URL 解析)
156 */ 158 */
157 export function extractExtensionFromFile(fileNameOrItem) { 159 export function extractExtensionFromFile(fileNameOrItem) {
...@@ -167,12 +169,14 @@ export function extractExtensionFromFile(fileNameOrItem) { ...@@ -167,12 +169,14 @@ export function extractExtensionFromFile(fileNameOrItem) {
167 if (fileNameOrItem.fileName) { 169 if (fileNameOrItem.fileName) {
168 extension = extractExtensionFromString(fileNameOrItem.fileName); 170 extension = extractExtensionFromString(fileNameOrItem.fileName);
169 } 171 }
170 - // 如果 fileName 没有扩展名,尝试从 src 解析 172 +
171 - else if (fileNameOrItem.src) { 173 + // 如果 fileName 没有扩展名(extension 仍为空),继续尝试其他字段
174 + if (!extension && fileNameOrItem.src) {
172 extension = extractExtensionFromString(fileNameOrItem.src); 175 extension = extractExtensionFromString(fileNameOrItem.src);
173 } 176 }
174 - // 如果 src 也没有,尝试从 downloadUrl 解析 177 +
175 - else if (fileNameOrItem.downloadUrl) { 178 + // 如果 src 也没有扩展名,继续尝试 downloadUrl
179 + if (!extension && fileNameOrItem.downloadUrl) {
176 extension = extractExtensionFromString(fileNameOrItem.downloadUrl); 180 extension = extractExtensionFromString(fileNameOrItem.downloadUrl);
177 } 181 }
178 } 182 }
......
1 +/**
2 + * 计划书字段值转换工具
3 + *
4 + * @description 提供各种数据格式的转换函数,用于表单数据提交前的格式化
5 + * @module utils/planFieldTransformers
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + */
9 +
10 +import { TRANSFORM_TYPES } from '@/config/plan-fields'
11 +
12 +/**
13 + * 分转元
14 + * @description 将分(整数)转换为元(浮点数,保留2位小数)
15 + * @param {number|string} value - 分值(如 10000)
16 + * @returns {string|null} 元值(如 "100.00")
17 + *
18 + * @example
19 + * fenToYuan(10000) // "100.00"
20 + * fenToYuan(0) // "0.00"
21 + * fenToYuan(null) // null
22 + */
23 +export function fenToYuan(value) {
24 + // 空字符串返回 null
25 + if (value === null || value === '') {
26 + return null
27 + }
28 + // undefined 保持原值(不转换)
29 + if (value === undefined) {
30 + return undefined
31 + }
32 +
33 + const numValue = parseFloat(value)
34 + if (Number.isNaN(numValue)) {
35 + return null
36 + }
37 +
38 + return (numValue / 100).toFixed(2) // Returns string "100.00"
39 +}
40 +
41 +/**
42 + * 元转分
43 + * @description 将元(浮点数)转换为分(整数)
44 + * @param {number|string} value - 元值(如 "100.00")
45 + * @returns {number|null} 分值(如 10000)
46 + *
47 + * @example
48 + * yuanToFen("100.00") // 10000
49 + * yuanToFen(0) // 0
50 + * yuanToFen(null) // null
51 + */
52 +export function yuanToFen(value) {
53 + if (value === null || value === undefined || value === '') {
54 + return null
55 + }
56 +
57 + const numValue = parseFloat(value)
58 + if (Number.isNaN(numValue)) {
59 + return null
60 + }
61 +
62 + return Math.round(numValue * 100) // Returns number 10000
63 +}
64 +
65 +/**
66 + * 年龄格式化
67 + * @description 将数字年龄格式化为 "XX岁" 字符串
68 + * @param {number} value - 年龄数字
69 + * @returns {string|null} 格式化后的年龄
70 + *
71 + * @example
72 + * formatAge(25) // "25岁"
73 + * formatAge(null) // null
74 + */
75 +export function formatAge(value) {
76 + if (value === null || value === undefined) {
77 + return null
78 + }
79 + return `${value}岁`
80 +}
81 +
82 +/**
83 + * 无需转换
84 + * @description 直接返回原值
85 + * @param {*} value - 任意值
86 + * @returns {*} 返回原值
87 + */
88 +export function noneTransform(value) {
89 + return value
90 +}
91 +
92 +/**
93 + * 转换器映射表
94 + * @type {Object<string, Function>}
95 + */
96 +export const FIELD_TRANSFORMERS = {
97 + [TRANSFORM_TYPES.FEN_TO_YUAN]: fenToYuan,
98 + [TRANSFORM_TYPES.YUAN_TO_FEN]: yuanToFen,
99 + [TRANSFORM_TYPES.NONE]: noneTransform
100 +}
101 +
102 +/**
103 + * 执行字段值转换
104 + * @param {*} value - 原始值
105 + * @param {string} transformType - 转换类型
106 + * @returns {*} 转换后的值
107 + *
108 + * @example
109 + * transformFieldValue(10000, 'fen_to_yuan') // "100.00"
110 + * transformFieldValue("100.00", 'none') // "100.00"
111 + */
112 +export function transformFieldValue(value, transformType) {
113 + if (!transformType || transformType === TRANSFORM_TYPES.NONE) {
114 + return value
115 + }
116 +
117 + const transformer = FIELD_TRANSFORMERS[transformType]
118 +
119 + if (!transformer) {
120 + console.warn(`[planFieldTransformers] 未知的转换类型: ${transformType}`)
121 + return value
122 + }
123 +
124 + return transformer(value)
125 +}
126 +
127 +/**
128 + * 批量转换字段值
129 + * @param {Object} formData - 表单数据
130 + * @param {Object} fieldDefinitions - 字段定义映射
131 + * @returns {Object} 转换后的数据
132 + *
133 + * @example
134 + * batchTransformFields(
135 + * { coverage: 10000, name: 'Test' },
136 + * { coverage: { transform: 'fen_to_yuan' } }
137 + * ) // { coverage: '100.00', name: 'Test' }
138 + */
139 +export function batchTransformFields(formData, fieldDefinitions) {
140 + const result = { ...formData }
141 +
142 + for (const [key, value] of Object.entries(result)) {
143 + const definition = fieldDefinitions[key]
144 +
145 + if (!definition || !definition.transform) {
146 + continue
147 + }
148 +
149 + result[key] = transformFieldValue(value, definition.transform)
150 + }
151 +
152 + return result
153 +}
154 +
155 +/**
156 + * 反向转换(从 API 响应转换回表单格式)
157 + * @param {Object} data - API 返回的数据
158 + * @param {Object} fieldDefinitions - 字段定义映射
159 + * @returns {Object} 转换后的表单数据
160 + *
161 + * @example
162 + * reverseTransformFields(
163 + * { annual_premium: '100.00', name: 'Test' },
164 + * { annual_premium: { api_field: 'annual_premium', transform: 'fen_to_yuan' } }
165 + * ) // { annual_premium: 10000, name: 'Test' }
166 + */
167 +export function reverseTransformFields(data, fieldDefinitions) {
168 + const result = {}
169 +
170 + for (const [formKey, definition] of Object.entries(fieldDefinitions)) {
171 + // 跳过没有 api_field 的字段
172 + if (!definition.api_field) {
173 + continue
174 + }
175 +
176 + const apiValue = data[definition.api_field]
177 + if (apiValue === undefined || apiValue === null) {
178 + continue
179 + }
180 +
181 + // 没有转换类型,直接使用原值
182 + if (!definition.transform || definition.transform === TRANSFORM_TYPES.NONE) {
183 + result[formKey] = apiValue
184 + continue
185 + }
186 +
187 + // 获取反向转换器
188 + let reverseTransform = definition.transform
189 +
190 + if (reverseTransform === TRANSFORM_TYPES.FEN_TO_YUAN) {
191 + reverseTransform = TRANSFORM_TYPES.YUAN_TO_FEN
192 + } else if (reverseTransform === TRANSFORM_TYPES.YUAN_TO_FEN) {
193 + reverseTransform = TRANSFORM_TYPES.FEN_TO_YUAN
194 + } else {
195 + // 未知转换类型,使用原值
196 + result[formKey] = apiValue
197 + continue
198 + }
199 +
200 + result[formKey] = transformFieldValue(apiValue, reverseTransform)
201 + }
202 +
203 + return result
204 +}
1 +/**
2 + * 动态验证系统
3 + *
4 + * @description 提供可配置的字段验证功能,支持同步/异步验证
5 + * @module utils/planFieldValidation
6 + * @author Claude Code
7 + * @created 2026-02-14
8 + */
9 +
10 +/**
11 + * 验证结果类型
12 + * @typedef {Object} ValidationResult
13 + * @property {boolean} valid - 是否通过
14 + * @property {string} [error] - 错误信息
15 + */
16 +
17 +/**
18 + * 内置验证规则
19 + */
20 +export const VALIDATION_RULES = {
21 + REQUIRED: 'required',
22 + MIN: 'min',
23 + MAX: 'max',
24 + RANGE: 'range',
25 + PATTERN: 'pattern',
26 + CUSTOM: 'custom'
27 +}
28 +
29 +/**
30 + * 执行字段验证
31 + *
32 + * @param {*} value - 待验证的值
33 + * @param {Object} rules - 验证规则配置
34 + * @param {Object} context - 验证上下文(包含 formData 等)
35 + * @returns {ValidationResult} 验证结果
36 + *
37 + * @example
38 + * // 必填验证
39 + * validateField(null, { required: true })
40 + * // => { valid: false, error: '该字段为必填' }
41 + *
42 + * // 最小长度验证
43 + * validateField('ab', { min: 3 })
44 + * // => { valid: false, error: '至少需要3个字符' }
45 + *
46 + * // 自定义验证
47 + * validateField(25, { custom: (value) => value >= 18 })
48 + * // => { valid: false, error: '年龄必须满18岁' }
49 + */
50 +export function validateField(value, rules = {}, context = {}) {
51 + // 必填检查
52 + if (rules.required) {
53 + // 函数形式的 required - 执行自定义验证
54 + if (typeof rules.required === 'function') {
55 + const result = rules.required(value, context)
56 + if (!result) {
57 + return {
58 + valid: false,
59 + error: rules.requiredMessage || '该字段为必填'
60 + }
61 + }
62 + } else if (!isNotEmpty(value)) {
63 + // 布尔值形式的 required - 检查是否为空
64 + return {
65 + valid: false,
66 + error: rules.requiredMessage || '该字段为必填'
67 + }
68 + }
69 + }
70 +
71 + // 最小长度
72 + if (rules.min !== undefined && value && value.length < rules.min) {
73 + return {
74 + valid: false,
75 + error: rules.minMessage || `至少需要${rules.min}个字符`
76 + }
77 + }
78 +
79 + // 最大长度
80 + if (rules.max !== undefined && value && value.length > rules.max) {
81 + return {
82 + valid: false,
83 + error: rules.maxMessage || `最多${rules.max}个字符`
84 + }
85 + }
86 +
87 + // 数值范围
88 + if (rules.range) {
89 + const numValue = parseFloat(value)
90 + if (Number.isNaN(numValue)) {
91 + return {
92 + valid: false,
93 + error: '请输入有效数字'
94 + }
95 + }
96 + const [min, max] = rules.range
97 + if ((min !== undefined && numValue < min) || (max !== undefined && numValue > max)) {
98 + return {
99 + valid: false,
100 + error: rules.rangeMessage || `请输入${min || 0}-${max || '∞'}之间的数值`
101 + }
102 + }
103 + }
104 +
105 + // 正则表达式
106 + if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
107 + return {
108 + valid: false,
109 + error: rules.patternMessage || '格式不正确'
110 + }
111 + }
112 +
113 + // 自定义验证函数
114 + if (rules.custom && typeof rules.custom === 'function') {
115 + const result = rules.custom(value, context)
116 + if (!result) {
117 + return {
118 + valid: false,
119 + error: rules.customMessage || '验证失败'
120 + }
121 + }
122 + }
123 +
124 + // 全部通过
125 + return { valid: true }
126 +}
127 +
128 +/**
129 + * 批量验证表单数据
130 + *
131 + * @param {Object} formData - 表单数据
132 + * @param {Object} fieldDefinitions - 字段定义
133 + * @returns {Object} 验证结果 { valid: boolean, errors: Object }
134 + *
135 + * @example
136 + * const result = validateForm(formData, fieldDefinitions)
137 + * if (result.valid) {
138 + * // 提交
139 + * } else {
140 + * // 显示错误
141 + * console.log(result.errors)
142 + * }
143 + */
144 +export function validateForm(formData, fieldDefinitions) {
145 + const errors = {}
146 +
147 + for (const [key, value] of Object.entries(formData)) {
148 + // 跳过空值(如果非必填)
149 + if (value === null || value === undefined || value === '') {
150 + const definition = fieldDefinitions[key]
151 + if (definition?.validation) {
152 + const rules = definition.validation
153 +
154 + // 检查是否真正的必填:先调用 required 规则判断
155 + const isRequired = rules.required ? typeof rules.required === 'function' ? rules.required(value) : true : false
156 +
157 + // 如果是必填字段且值为空,验证失败
158 + if (isRequired && !isNotEmpty(value)) {
159 + errors[key] = '该字段为必填'
160 + }
161 + }
162 + continue
163 + }
164 +
165 + // 验证非空值
166 + const definition = fieldDefinitions[key]
167 + if (definition?.validation) {
168 + const rules = definition.validation
169 +
170 + // 将 required 规则放在第一位,确保它被优先检查
171 + const orderedRules = {}
172 + if (rules.required) {
173 + orderedRules.required = rules.required
174 + }
175 + for (const ruleKey in rules) {
176 + if (ruleKey !== 'required') {
177 + orderedRules[ruleKey] = rules[ruleKey]
178 + }
179 + }
180 +
181 + const result = validateField(value, orderedRules, { formData, ...formData })
182 + if (!result.valid) {
183 + errors[key] = result.error
184 + }
185 + }
186 + }
187 +
188 + return {
189 + valid: Object.keys(errors).length === 0,
190 + errors
191 + }
192 +}
193 +
194 +/**
195 + * 检查值是否非空
196 + *
197 + * @param {*} value - 待检查的值
198 + * @returns {boolean} 是否非空
199 + */
200 +export function isNotEmpty(value) {
201 + if (value === null || value === undefined) return false
202 + if (typeof value === 'string' && value.trim() === '') return false
203 + if (Array.isArray(value) && value.length === 0) return false
204 + return true
205 +}