refactor(plan): 完善计划书模板字段配置
- 更新 README.md 文档说明 - 更新 CHANGELOG.md 记录变更 - 优化 CriticalIllnessTemplate 字段配置 - 优化 LifeInsuranceTemplate 字段配置 - 优化 SavingsTemplate 字段配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
5 changed files
with
115 additions
and
21 deletions
| ... | @@ -58,6 +58,7 @@ pnpm lint | ... | @@ -58,6 +58,7 @@ pnpm lint |
| 58 | - ✅ **Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验 | 58 | - ✅ **Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验 |
| 59 | - ✅ **提交映射下沉** - 提交字段映射从容器迁移到模板配置 | 59 | - ✅ **提交映射下沉** - 提交字段映射从容器迁移到模板配置 |
| 60 | - ✅ **人寿/重疾同步** - 人寿与重疾模板改为 Schema 驱动 | 60 | - ✅ **人寿/重疾同步** - 人寿与重疾模板改为 Schema 驱动 |
| 61 | +- ✅ **校验提示优化** - 必填提示与百分比校验统一更准确 | ||
| 61 | 62 | ||
| 62 | ### 字段命名优化 | 63 | ### 字段命名优化 |
| 63 | - ✅ **提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method | 64 | - ✅ **提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method | ... | ... |
| 1 | +#### [2026-02-14] - 计划书必填校验与提示优化 | ||
| 2 | + | ||
| 3 | +### 修复 | ||
| 4 | +- 计划书模板必填规则统一判断,避免缺失字段未触发校验 | ||
| 5 | +- 必填提示文案按输入/选择类型调整为更准确的提示语 | ||
| 6 | +- 百分比字段空值不再误报校验错误 | ||
| 7 | + | ||
| 8 | +### 测试 | ||
| 9 | +- pnpm test(通过) | ||
| 10 | +- pnpm lint(存在历史警告) | ||
| 11 | + | ||
| 12 | +--- | ||
| 13 | + | ||
| 14 | +**详细信息**: | ||
| 15 | +- **影响文件**: src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue, src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue, README.md | ||
| 16 | +- **技术栈**: Vue 3, Taro, Vitest | ||
| 17 | +- **测试状态**: 已通过(lint 有警告) | ||
| 18 | +- **备注**: 统一必填与提示逻辑,补齐空值校验边界 | ||
| 19 | + | ||
| 20 | +--- | ||
| 21 | + | ||
| 1 | #### [2026-02-14] - 提取字段分组修正 | 22 | #### [2026-02-14] - 提取字段分组修正 |
| 2 | 23 | ||
| 3 | ### 变更 | 24 | ### 变更 | ... | ... |
| ... | @@ -283,6 +283,27 @@ const onPercentageInput = (value, key) => { | ... | @@ -283,6 +283,27 @@ const onPercentageInput = (value, key) => { |
| 283 | form[key] = cleaned | 283 | form[key] = cleaned |
| 284 | } | 284 | } |
| 285 | 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 | ||
| 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}` | ||
| 299 | + } | ||
| 300 | + return `请输入${label}` | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +const isFieldRequired = (field) => { | ||
| 304 | + return field?.required === true || field?.required === undefined | ||
| 305 | +} | ||
| 306 | + | ||
| 286 | /** | 307 | /** |
| 287 | * 表单校验(基于 Schema) | 308 | * 表单校验(基于 Schema) |
| 288 | * @returns {boolean} 校验是否通过 | 309 | * @returns {boolean} 校验是否通过 |
| ... | @@ -295,19 +316,22 @@ const validate = () => { | ... | @@ -295,19 +316,22 @@ const validate = () => { |
| 295 | continue | 316 | continue |
| 296 | } | 317 | } |
| 297 | 318 | ||
| 298 | - if (field.required) { | 319 | + if (isFieldRequired(field)) { |
| 299 | const value = form[field.key] | 320 | const value = form[field.key] |
| 300 | - if (value === undefined || value === null || value === '') { | 321 | + if (isEmptyValue(value)) { |
| 301 | - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) | 322 | + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' }) |
| 302 | return false | 323 | return false |
| 303 | } | 324 | } |
| 304 | } | 325 | } |
| 305 | 326 | ||
| 306 | if (field.type === 'percentage' && isFieldVisible(field.key)) { | 327 | if (field.type === 'percentage' && isFieldVisible(field.key)) { |
| 307 | - const percentage = parseFloat(form[field.key]) | 328 | + const value = form[field.key] |
| 308 | - if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { | 329 | + if (!isEmptyValue(value)) { |
| 309 | - Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | 330 | + const percentage = parseFloat(value) |
| 310 | - return false | 331 | + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { |
| 332 | + Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | ||
| 333 | + return false | ||
| 334 | + } | ||
| 311 | } | 335 | } |
| 312 | } | 336 | } |
| 313 | } | 337 | } | ... | ... |
| ... | @@ -285,6 +285,27 @@ const onPercentageInput = (value, key) => { | ... | @@ -285,6 +285,27 @@ const onPercentageInput = (value, key) => { |
| 285 | form[key] = cleaned | 285 | form[key] = cleaned |
| 286 | } | 286 | } |
| 287 | 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 | ||
| 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}` | ||
| 301 | + } | ||
| 302 | + return `请输入${label}` | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +const isFieldRequired = (field) => { | ||
| 306 | + return field?.required === true || field?.required === undefined | ||
| 307 | +} | ||
| 308 | + | ||
| 288 | /** | 309 | /** |
| 289 | * 表单校验(基于 Schema) | 310 | * 表单校验(基于 Schema) |
| 290 | * @returns {boolean} 校验是否通过 | 311 | * @returns {boolean} 校验是否通过 |
| ... | @@ -297,19 +318,22 @@ const validate = () => { | ... | @@ -297,19 +318,22 @@ const validate = () => { |
| 297 | continue | 318 | continue |
| 298 | } | 319 | } |
| 299 | 320 | ||
| 300 | - if (field.required) { | 321 | + if (isFieldRequired(field)) { |
| 301 | const value = form[field.key] | 322 | const value = form[field.key] |
| 302 | - if (value === undefined || value === null || value === '') { | 323 | + if (isEmptyValue(value)) { |
| 303 | - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) | 324 | + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' }) |
| 304 | return false | 325 | return false |
| 305 | } | 326 | } |
| 306 | } | 327 | } |
| 307 | 328 | ||
| 308 | if (field.type === 'percentage' && isFieldVisible(field.key)) { | 329 | if (field.type === 'percentage' && isFieldVisible(field.key)) { |
| 309 | - const percentage = parseFloat(form[field.key]) | 330 | + const value = form[field.key] |
| 310 | - if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { | 331 | + if (!isEmptyValue(value)) { |
| 311 | - Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | 332 | + const percentage = parseFloat(value) |
| 312 | - return false | 333 | + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { |
| 334 | + Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | ||
| 335 | + return false | ||
| 336 | + } | ||
| 313 | } | 337 | } |
| 314 | } | 338 | } |
| 315 | } | 339 | } | ... | ... |
| ... | @@ -377,6 +377,27 @@ const onPercentageInput = (value, key) => { | ... | @@ -377,6 +377,27 @@ const onPercentageInput = (value, key) => { |
| 377 | form[key] = cleaned | 377 | form[key] = cleaned |
| 378 | } | 378 | } |
| 379 | 379 | ||
| 380 | +const isEmptyValue = (value) => { | ||
| 381 | + if (value === null || value === undefined) return true | ||
| 382 | + if (typeof value === 'string' && value.trim() === '') return true | ||
| 383 | + if (Array.isArray(value) && value.length === 0) return true | ||
| 384 | + return false | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +const getRequiredMessage = (field) => { | ||
| 388 | + if (field?.placeholder) return field.placeholder | ||
| 389 | + const label = field?.label || '必填信息' | ||
| 390 | + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age'] | ||
| 391 | + if (selectTypes.includes(field?.type)) { | ||
| 392 | + return `请选择${label}` | ||
| 393 | + } | ||
| 394 | + return `请输入${label}` | ||
| 395 | +} | ||
| 396 | + | ||
| 397 | +const isFieldRequired = (field) => { | ||
| 398 | + return field?.required === true || field?.required === undefined | ||
| 399 | +} | ||
| 400 | + | ||
| 380 | /** | 401 | /** |
| 381 | * 表单校验 | 402 | * 表单校验 |
| 382 | * @returns {boolean} 是否通过校验 | 403 | * @returns {boolean} 是否通过校验 |
| ... | @@ -393,19 +414,22 @@ const validate = () => { | ... | @@ -393,19 +414,22 @@ const validate = () => { |
| 393 | continue | 414 | continue |
| 394 | } | 415 | } |
| 395 | 416 | ||
| 396 | - if (field.required) { | 417 | + if (isFieldRequired(field)) { |
| 397 | const value = form[field.key] | 418 | const value = form[field.key] |
| 398 | - if (value === undefined || value === null || value === '') { | 419 | + if (isEmptyValue(value)) { |
| 399 | - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) | 420 | + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' }) |
| 400 | return false | 421 | return false |
| 401 | } | 422 | } |
| 402 | } | 423 | } |
| 403 | 424 | ||
| 404 | if (field.type === 'percentage' && isFieldVisible(field.key)) { | 425 | if (field.type === 'percentage' && isFieldVisible(field.key)) { |
| 405 | - const percentage = parseFloat(form[field.key]) | 426 | + const value = form[field.key] |
| 406 | - if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { | 427 | + if (!isEmptyValue(value)) { |
| 407 | - Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | 428 | + const percentage = parseFloat(value) |
| 408 | - return false | 429 | + if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { |
| 430 | + Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) | ||
| 431 | + return false | ||
| 432 | + } | ||
| 409 | } | 433 | } |
| 410 | } | 434 | } |
| 411 | } | 435 | } | ... | ... |
-
Please register or login to post a comment