fix(plan): 修复储蓄类多阶段模版保存问题
- 添加 stages 数据从 props.modelValue 恢复的逻辑 - 修复 validate() 中单阶段字段在多阶段模式下被错误校验 - 添加详细的调试日志 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
2 changed files
with
119 additions
and
2 deletions
| ... | @@ -335,3 +335,13 @@ | ... | @@ -335,3 +335,13 @@ |
| 335 | 335 | ||
| 336 | **变更摘要**: | 336 | **变更摘要**: |
| 337 | - CHANGELOG 自动更新功能配置 | 337 | - CHANGELOG 自动更新功能配置 |
| 338 | + | ||
| 339 | +### 2026-02-28 - 修复储蓄类多阶段模版保存问题 | ||
| 340 | + | ||
| 341 | +**影响文件**: | ||
| 342 | +- `src/components/plan/PlanTemplates/SavingsTemplate.vue` | ||
| 343 | + | ||
| 344 | +**变更摘要**: | ||
| 345 | +- 修复多阶段模式下 stages 数据无法从 props.modelValue 恢复的问题 | ||
| 346 | +- 修复 validate() 函数中单阶段字段被错误校验的问题 | ||
| 347 | +- 添加详细的调试日志以方便排查问题 | ... | ... |
| ... | @@ -390,12 +390,16 @@ const canRemoveStage = computed(() => { | ... | @@ -390,12 +390,16 @@ const canRemoveStage = computed(() => { |
| 390 | * 创建空的阶段数据 | 390 | * 创建空的阶段数据 |
| 391 | * @returns {Object} 空阶段对象 | 391 | * @returns {Object} 空阶段对象 |
| 392 | */ | 392 | */ |
| 393 | -const createStage = () => ({ | 393 | +const createStage = () => { |
| 394 | + const stage = { | ||
| 394 | annual_withdrawal_amount: null, | 395 | annual_withdrawal_amount: null, |
| 395 | withdrawal_start_age: null, | 396 | withdrawal_start_age: null, |
| 396 | withdrawal_period: null, | 397 | withdrawal_period: null, |
| 397 | annual_increase_percentage: null | 398 | annual_increase_percentage: null |
| 398 | -}) | 399 | + } |
| 400 | + console.log('createStage() 创建:', stage) | ||
| 401 | + return stage | ||
| 402 | +} | ||
| 399 | 403 | ||
| 400 | /** | 404 | /** |
| 401 | * 初始化阶段数据 | 405 | * 初始化阶段数据 |
| ... | @@ -404,16 +408,22 @@ const createStage = () => ({ | ... | @@ -404,16 +408,22 @@ const createStage = () => ({ |
| 404 | * - 年龄 ≥ 12岁:初始 1 组 | 408 | * - 年龄 ≥ 12岁:初始 1 组 |
| 405 | */ | 409 | */ |
| 406 | const initializeStages = () => { | 410 | const initializeStages = () => { |
| 411 | + console.log('=== initializeStages() 调用 ===') | ||
| 412 | + console.log('form.age:', form.age) | ||
| 407 | const age = parseInt(form.age) || 0 | 413 | const age = parseInt(form.age) || 0 |
| 408 | const threshold = multiStageConfig.value.age_threshold || 12 | 414 | const threshold = multiStageConfig.value.age_threshold || 12 |
| 415 | + console.log('age:', age, 'threshold:', threshold) | ||
| 409 | 416 | ||
| 410 | if (age < threshold) { | 417 | if (age < threshold) { |
| 411 | // 固定 3 组 | 418 | // 固定 3 组 |
| 412 | stages.value = [createStage(), createStage(), createStage()] | 419 | stages.value = [createStage(), createStage(), createStage()] |
| 420 | + console.log('初始化 3 组阶段') | ||
| 413 | } else { | 421 | } else { |
| 414 | // 初始 1 组 | 422 | // 初始 1 组 |
| 415 | stages.value = [createStage()] | 423 | stages.value = [createStage()] |
| 424 | + console.log('初始化 1 组阶段') | ||
| 416 | } | 425 | } |
| 426 | + console.log('stages.value:', stages.value) | ||
| 417 | } | 427 | } |
| 418 | 428 | ||
| 419 | /** | 429 | /** |
| ... | @@ -442,6 +452,8 @@ const removeStage = (index) => { | ... | @@ -442,6 +452,8 @@ const removeStage = (index) => { |
| 442 | watch( | 452 | watch( |
| 443 | stages, | 453 | stages, |
| 444 | (newStages) => { | 454 | (newStages) => { |
| 455 | + console.log('=== stages watch 触发 ===') | ||
| 456 | + console.log('newStages:', JSON.parse(JSON.stringify(newStages))) | ||
| 445 | // 清理每个阶段的 undefined 值为 null | 457 | // 清理每个阶段的 undefined 值为 null |
| 446 | const cleanedStages = newStages.map(stage => ({ | 458 | const cleanedStages = newStages.map(stage => ({ |
| 447 | annual_withdrawal_amount: stage.annual_withdrawal_amount ?? null, | 459 | annual_withdrawal_amount: stage.annual_withdrawal_amount ?? null, |
| ... | @@ -449,7 +461,10 @@ watch( | ... | @@ -449,7 +461,10 @@ watch( |
| 449 | withdrawal_period: stage.withdrawal_period ?? null, | 461 | withdrawal_period: stage.withdrawal_period ?? null, |
| 450 | annual_increase_percentage: stage.annual_increase_percentage ?? null | 462 | annual_increase_percentage: stage.annual_increase_percentage ?? null |
| 451 | })) | 463 | })) |
| 464 | + console.log('cleanedStages:', cleanedStages) | ||
| 465 | + console.log('设置 form.withdrawal_stages') | ||
| 452 | form.withdrawal_stages = cleanedStages | 466 | form.withdrawal_stages = cleanedStages |
| 467 | + console.log('form.withdrawal_stages 已更新:', form.withdrawal_stages) | ||
| 453 | }, | 468 | }, |
| 454 | { deep: true } | 469 | { deep: true } |
| 455 | ) | 470 | ) |
| ... | @@ -476,8 +491,14 @@ watch( | ... | @@ -476,8 +491,14 @@ watch( |
| 476 | watch( | 491 | watch( |
| 477 | isMultiStageMode, | 492 | isMultiStageMode, |
| 478 | (enabled) => { | 493 | (enabled) => { |
| 494 | + console.log('=== isMultiStageMode watch ===') | ||
| 495 | + console.log('enabled:', enabled) | ||
| 496 | + console.log('stages.value.length:', stages.value.length) | ||
| 479 | if (enabled && stages.value.length === 0) { | 497 | if (enabled && stages.value.length === 0) { |
| 498 | + console.log('条件满足,调用 initializeStages()') | ||
| 480 | initializeStages() | 499 | initializeStages() |
| 500 | + } else { | ||
| 501 | + console.log('跳过初始化') | ||
| 481 | } | 502 | } |
| 482 | }, | 503 | }, |
| 483 | { immediate: true } | 504 | { immediate: true } |
| ... | @@ -529,8 +550,13 @@ const initializeForm = (value) => { | ... | @@ -529,8 +550,13 @@ const initializeForm = (value) => { |
| 529 | watch( | 550 | watch( |
| 530 | () => props.modelValue, | 551 | () => props.modelValue, |
| 531 | (newVal) => { | 552 | (newVal) => { |
| 553 | + console.log('=== modelValue watch 触发 ===') | ||
| 554 | + console.log('newVal:', newVal) | ||
| 555 | + console.log('isMultiStageMode:', isMultiStageMode.value) | ||
| 556 | + | ||
| 532 | if (!newVal) { | 557 | if (!newVal) { |
| 533 | // null 或 undefined:清空 | 558 | // null 或 undefined:清空 |
| 559 | + console.log('newVal 为空,清空表单') | ||
| 534 | Object.keys(form).forEach(key => delete form[key]) | 560 | Object.keys(form).forEach(key => delete form[key]) |
| 535 | previousModelValue = null | 561 | previousModelValue = null |
| 536 | return | 562 | return |
| ... | @@ -543,10 +569,12 @@ watch( | ... | @@ -543,10 +569,12 @@ watch( |
| 543 | 569 | ||
| 544 | if (isReset) { | 570 | if (isReset) { |
| 545 | // 父组件重置了:清空表单 | 571 | // 父组件重置了:清空表单 |
| 572 | + console.log('检测到重置,清空表单') | ||
| 546 | initializeForm(newVal) | 573 | initializeForm(newVal) |
| 547 | previousModelValue = newVal | 574 | previousModelValue = newVal |
| 548 | } else { | 575 | } else { |
| 549 | // 正常更新:合并新字段,保留默认值逻辑 | 576 | // 正常更新:合并新字段,保留默认值逻辑 |
| 577 | + console.log('正常更新表单数据') | ||
| 550 | const defaults = getSchemaDefaults(newVal) | 578 | const defaults = getSchemaDefaults(newVal) |
| 551 | Object.assign(form, { | 579 | Object.assign(form, { |
| 552 | ...newVal, | 580 | ...newVal, |
| ... | @@ -559,6 +587,36 @@ watch( | ... | @@ -559,6 +587,36 @@ watch( |
| 559 | withdrawal_period_fixed: newVal.withdrawal_period_fixed ?? null | 587 | withdrawal_period_fixed: newVal.withdrawal_period_fixed ?? null |
| 560 | }) | 588 | }) |
| 561 | previousModelValue = newVal | 589 | previousModelValue = newVal |
| 590 | + | ||
| 591 | + // 恢复 stages 数据(多阶段模式) | ||
| 592 | + console.log('检查是否需要恢复 stages 数据...') | ||
| 593 | + console.log(' isMultiStageMode:', isMultiStageMode.value) | ||
| 594 | + console.log(' newVal.withdrawal_stages:', newVal.withdrawal_stages) | ||
| 595 | + | ||
| 596 | + if (isMultiStageMode.value && newVal.withdrawal_stages && Array.isArray(newVal.withdrawal_stages)) { | ||
| 597 | + console.log('✅ 开始恢复 stages 数据') | ||
| 598 | + console.log(' 当前 stages.value:', stages.value) | ||
| 599 | + console.log(' 新数据 withdrawal_stages:', newVal.withdrawal_stages) | ||
| 600 | + | ||
| 601 | + // 深度比较,避免覆盖用户正在编辑的数据 | ||
| 602 | + const currentStagesStr = JSON.stringify(stages.value) | ||
| 603 | + const newStagesStr = JSON.stringify(newVal.withdrawal_stages) | ||
| 604 | + console.log(' 深度比较:', currentStagesStr === newStagesStr ? '相同' : '不同') | ||
| 605 | + | ||
| 606 | + if (currentStagesStr !== newStagesStr) { | ||
| 607 | + stages.value = newVal.withdrawal_stages.map(stage => ({ | ||
| 608 | + annual_withdrawal_amount: stage.annual_withdrawal_amount ?? null, | ||
| 609 | + withdrawal_start_age: stage.withdrawal_start_age ?? null, | ||
| 610 | + withdrawal_period: stage.withdrawal_period ?? null, | ||
| 611 | + annual_increase_percentage: stage.annual_increase_percentage ?? null | ||
| 612 | + })) | ||
| 613 | + console.log(' ✅ stages 已恢复:', stages.value) | ||
| 614 | + } else { | ||
| 615 | + console.log(' 跳过恢复(数据相同)') | ||
| 616 | + } | ||
| 617 | + } else { | ||
| 618 | + console.log(' ❌ 不需要恢复 stages') | ||
| 619 | + } | ||
| 562 | } | 620 | } |
| 563 | }, | 621 | }, |
| 564 | { immediate: true } | 622 | { immediate: true } |
| ... | @@ -674,10 +732,18 @@ const onPercentageInput = (value, key) => { | ... | @@ -674,10 +732,18 @@ const onPercentageInput = (value, key) => { |
| 674 | } | 732 | } |
| 675 | 733 | ||
| 676 | const isEmptyValue = (value) => { | 734 | const isEmptyValue = (value) => { |
| 735 | + const result = (() => { | ||
| 677 | if (value === null || value === undefined) return true | 736 | if (value === null || value === undefined) return true |
| 678 | if (typeof value === 'string' && value.trim() === '') return true | 737 | if (typeof value === 'string' && value.trim() === '') return true |
| 679 | if (Array.isArray(value) && value.length === 0) return true | 738 | if (Array.isArray(value) && value.length === 0) return true |
| 680 | return false | 739 | return false |
| 740 | + })() | ||
| 741 | + // 只在调用栈包含 validate 时打印日志,避免过多输出 | ||
| 742 | + const stack = new Error().stack || '' | ||
| 743 | + if (stack.includes('validate')) { | ||
| 744 | + console.log(`isEmptyValue(${value} [${typeof value}]) = ${result}`) | ||
| 745 | + } | ||
| 746 | + return result | ||
| 681 | } | 747 | } |
| 682 | 748 | ||
| 683 | const getRequiredMessage = (field) => { | 749 | const getRequiredMessage = (field) => { |
| ... | @@ -703,8 +769,14 @@ const isFieldRequired = (field) => { | ... | @@ -703,8 +769,14 @@ const isFieldRequired = (field) => { |
| 703 | * @returns {boolean} 校验是否通过 | 769 | * @returns {boolean} 校验是否通过 |
| 704 | */ | 770 | */ |
| 705 | const validate = () => { | 771 | const validate = () => { |
| 772 | + console.log('=== validate() 开始 ===') | ||
| 773 | + console.log('form 数据:', form) | ||
| 774 | + console.log('form.withdrawal_mode:', form.withdrawal_mode) | ||
| 775 | + console.log('isMultiStageMode:', isMultiStageMode.value) | ||
| 776 | + | ||
| 706 | // 1. 基础字段校验(单阶段和多阶段通用) | 777 | // 1. 基础字段校验(单阶段和多阶段通用) |
| 707 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] | 778 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] |
| 779 | + console.log('需要校验的基础字段数:', fields.length) | ||
| 708 | 780 | ||
| 709 | // 年龄与出生年月日二选一校验 | 781 | // 年龄与出生年月日二选一校验 |
| 710 | const hasAge = !isEmptyValue(form.age) | 782 | const hasAge = !isEmptyValue(form.age) |
| ... | @@ -731,6 +803,21 @@ const validate = () => { | ... | @@ -731,6 +803,21 @@ const validate = () => { |
| 731 | // 跳过年龄字段的单独校验(已和生日一起校验) | 803 | // 跳过年龄字段的单独校验(已和生日一起校验) |
| 732 | if (field.key === 'age') continue | 804 | if (field.key === 'age') continue |
| 733 | 805 | ||
| 806 | + // 多阶段模式 + 选择"指定提取金额"时,跳过单阶段字段的校验 | ||
| 807 | + // 这些字段在多阶段模式下由 validateMultiStage() 校验 | ||
| 808 | + if (isMultiStageMode.value && form.withdrawal_mode === '指定提取金额') { | ||
| 809 | + const skipFields = [ | ||
| 810 | + 'annual_withdrawal_amount', | ||
| 811 | + 'withdrawal_start_age_specified', | ||
| 812 | + 'withdrawal_period_specified', | ||
| 813 | + 'annual_increase_percentage' | ||
| 814 | + ] | ||
| 815 | + if (skipFields.includes(field.key)) { | ||
| 816 | + console.log(`跳过单阶段字段校验: ${field.key}`) | ||
| 817 | + continue | ||
| 818 | + } | ||
| 819 | + } | ||
| 820 | + | ||
| 734 | if (isFieldRequired(field)) { | 821 | if (isFieldRequired(field)) { |
| 735 | const value = form[field.key] | 822 | const value = form[field.key] |
| 736 | if (isEmptyValue(value)) { | 823 | if (isEmptyValue(value)) { |
| ... | @@ -766,25 +853,42 @@ const validate = () => { | ... | @@ -766,25 +853,42 @@ const validate = () => { |
| 766 | * @returns {boolean} 校验是否通过 | 853 | * @returns {boolean} 校验是否通过 |
| 767 | */ | 854 | */ |
| 768 | const validateMultiStage = () => { | 855 | const validateMultiStage = () => { |
| 856 | + console.log('=== validateMultiStage 开始 ===') | ||
| 857 | + console.log('stages.value:', stages.value) | ||
| 858 | + console.log('stages.value.length:', stages.value.length) | ||
| 859 | + | ||
| 769 | // 多阶段字段校验 | 860 | // 多阶段字段校验 |
| 770 | for (let i = 0; i < stages.value.length; i++) { | 861 | for (let i = 0; i < stages.value.length; i++) { |
| 771 | const stage = stages.value[i] | 862 | const stage = stages.value[i] |
| 772 | const stageLabel = `阶段${i + 1}` | 863 | const stageLabel = `阶段${i + 1}` |
| 773 | 864 | ||
| 865 | + console.log(`--- 校验 ${stageLabel} ---`) | ||
| 866 | + console.log('stage 对象:', stage) | ||
| 867 | + console.log(' annual_withdrawal_amount:', stage.annual_withdrawal_amount, '类型:', typeof stage.annual_withdrawal_amount) | ||
| 868 | + console.log(' withdrawal_start_age:', stage.withdrawal_start_age, '类型:', typeof stage.withdrawal_start_age) | ||
| 869 | + console.log(' withdrawal_period:', stage.withdrawal_period, '类型:', typeof stage.withdrawal_period) | ||
| 870 | + console.log(' annual_increase_percentage:', stage.annual_increase_percentage, '类型:', typeof stage.annual_increase_percentage) | ||
| 871 | + | ||
| 774 | // 每年提取金额(必填) | 872 | // 每年提取金额(必填) |
| 873 | + console.log('检查 annual_withdrawal_amount isEmptyValue:', isEmptyValue(stage.annual_withdrawal_amount)) | ||
| 775 | if (isEmptyValue(stage.annual_withdrawal_amount)) { | 874 | if (isEmptyValue(stage.annual_withdrawal_amount)) { |
| 875 | + console.log(`❌ ${stageLabel}:每年提取金额为空`) | ||
| 776 | Taro.showToast({ title: `${stageLabel}:请输入每年提取金额`, icon: 'none' }) | 876 | Taro.showToast({ title: `${stageLabel}:请输入每年提取金额`, icon: 'none' }) |
| 777 | return false | 877 | return false |
| 778 | } | 878 | } |
| 779 | 879 | ||
| 780 | // 由几岁开始(必填) | 880 | // 由几岁开始(必填) |
| 881 | + console.log('检查 withdrawal_start_age isEmptyValue:', isEmptyValue(stage.withdrawal_start_age)) | ||
| 781 | if (isEmptyValue(stage.withdrawal_start_age)) { | 882 | if (isEmptyValue(stage.withdrawal_start_age)) { |
| 883 | + console.log(`❌ ${stageLabel}:withdrawal_start_age 为空`) | ||
| 782 | Taro.showToast({ title: `${stageLabel}:请输入由几岁开始`, icon: 'none' }) | 884 | Taro.showToast({ title: `${stageLabel}:请输入由几岁开始`, icon: 'none' }) |
| 783 | return false | 885 | return false |
| 784 | } | 886 | } |
| 785 | 887 | ||
| 786 | // 提取期(必填) | 888 | // 提取期(必填) |
| 889 | + console.log('检查 withdrawal_period isEmptyValue:', isEmptyValue(stage.withdrawal_period)) | ||
| 787 | if (isEmptyValue(stage.withdrawal_period)) { | 890 | if (isEmptyValue(stage.withdrawal_period)) { |
| 891 | + console.log(`❌ ${stageLabel}:withdrawal_period 为空`) | ||
| 788 | Taro.showToast({ title: `${stageLabel}:请选择提取期`, icon: 'none' }) | 892 | Taro.showToast({ title: `${stageLabel}:请选择提取期`, icon: 'none' }) |
| 789 | return false | 893 | return false |
| 790 | } | 894 | } |
| ... | @@ -793,12 +897,15 @@ const validateMultiStage = () => { | ... | @@ -793,12 +897,15 @@ const validateMultiStage = () => { |
| 793 | if (!isEmptyValue(stage.annual_increase_percentage)) { | 897 | if (!isEmptyValue(stage.annual_increase_percentage)) { |
| 794 | const percentage = parseFloat(stage.annual_increase_percentage) | 898 | const percentage = parseFloat(stage.annual_increase_percentage) |
| 795 | if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { | 899 | if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { |
| 900 | + console.log(`❌ ${stageLabel}:递增百分比超出范围`) | ||
| 796 | Taro.showToast({ title: `${stageLabel}:递增百分比请输入0-100之间的数值`, icon: 'none' }) | 901 | Taro.showToast({ title: `${stageLabel}:递增百分比请输入0-100之间的数值`, icon: 'none' }) |
| 797 | return false | 902 | return false |
| 798 | } | 903 | } |
| 799 | } | 904 | } |
| 905 | + console.log(`✅ ${stageLabel} 校验通过`) | ||
| 800 | } | 906 | } |
| 801 | 907 | ||
| 908 | + console.log('=== validateMultiStage 全部通过 ===') | ||
| 802 | return true | 909 | return true |
| 803 | } | 910 | } |
| 804 | 911 | ... | ... |
-
Please register or login to post a comment