hookehuyr

fix(plan): 修复储蓄类多阶段模版保存问题

- 添加 stages 数据从 props.modelValue 恢复的逻辑
- 修复 validate() 中单阶段字段在多阶段模式下被错误校验
- 添加详细的调试日志

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -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
......