hookehuyr

refactor(plan): 优化计划书字段配置管理

- 重构 useFieldValueTransform 为可复用组件,支持动态字段定义
- 优化 PlanFormContainer 字段映射逻辑,简化代码
- 添加计划书模板字段配置:withdrawal_start_age_simplified 等字段
- 改进字段值转换 composable 的灵活性和可配置性
- 更新 CHANGELOG.md 记录本次优化内容

影响文件:
- src/components/plan/PlanFormContainer.vue
- src/components/plan/PlanTemplates/*.vue
- src/composables/useFieldValueTransform.js
- src/composables/useFieldDependencies.js
- src/config/plan-templates.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -75,6 +75,11 @@ pnpm lint
-**转换逻辑修正** - 分元双向转换统一使用转换器
-**依赖检测测试** - 补充循环依赖检测单测与分组工具测试
### 计划书配置核查
-**配置应用核查** - 确认 plan-templates 已驱动表单渲染与提交映射,plan-fields 与字段关联/转换 composable 尚未接入生成链路
-**依赖与转换接入** - 表单可见性接入 useFieldDependencies,提交金额转换接入 useFieldValueTransform
-**提取字段拆分** - 指定提取金额与最高固定提取金额字段独立显示与提交映射
## 🆕 最新更新(2026-02-13)
### 权限与测试
......
#### [2026-02-14] - 提取字段分组修正
### 变更
- 指定提取金额字段改为独立 key,避免与固定提取字段混用
- 最高固定提取金额仅保留“按年岁:由几岁开始/提取期”
- 提交映射按提取模式覆盖正确的年龄与年期字段
### 测试
- pnpm test(通过)
- pnpm lint(存在历史警告)
---
**详细信息**
- **影响文件**: src/config/plan-templates.js, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue, src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue, src/components/plan/PlanFormContainer.vue, README.md
- **技术栈**: Vue 3, Taro, Vitest
- **测试状态**: 已通过(lint 有警告)
- **备注**: 提取字段按模式隔离,避免字段互相污染
---
#### [2026-02-14] - 计划书字段依赖与转换接入
### 变更
- useFieldDependencies 支持 Schema show_when 并接入模板可见性判断
- useFieldValueTransform 支持自定义映射并用于提交金额转换
### 测试
- pnpm test(通过,有组件解析警告)
- pnpm lint(存在历史警告)
---
**详细信息**
- **影响文件**: src/composables/useFieldDependencies.js, src/composables/useFieldValueTransform.js, src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue, src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanFormContainer.vue, README.md
- **技术栈**: Vue 3, Taro, Vitest
- **测试状态**: 已通过(lint 有警告)
- **备注**: 接入字段可见性与金额转换,保持现有表单输入单位为分
---
#### [2026-02-14] - 计划书配置链路核查
### 核查
- 确认 plan-templates.js 配置用于表单渲染与提交映射
- 确认 plan-fields.js 与字段关联/转换 composable 尚未接入计划书生成链路
### 测试
- 未运行(仅核查)
---
**详细信息**
- **影响文件**: src/config/plan-templates.js, src/config/plan-fields.js, src/composables/useFieldDependencies.js, src/composables/useFieldValueTransform.js, src/components/plan/PlanFormContainer.vue, src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue, src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue
- **技术栈**: Vue 3, Taro
- **测试状态**: 未运行
- **备注**: 仅进行链路核查,无代码改动
---
#### [2026-02-14] - 计划书字段分组与转换补齐
### 修复
......
......@@ -49,6 +49,7 @@ import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue
import SavingsTemplate from './PlanTemplates/SavingsTemplate.vue'
import { PLAN_TEMPLATES } from '@/config/plan-templates'
import { addAPI } from '@/api/plan'
import { useFieldValueTransform } from '@/composables/useFieldValueTransform'
/**
* 组件属性
......@@ -278,11 +279,13 @@ const submit = async () => {
payment_period: { api_field: 'payment_years' },
withdrawal_enabled: { api_field: 'allow_reduce_amount' },
withdrawal_mode: { api_field: 'withdrawal_option' },
withdrawal_start_age: { api_field: 'withdrawal_start_age' },
withdrawal_period: { api_field: 'withdrawal_period' },
withdrawal_method: { api_field: 'withdrawal_method' },
annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
annual_increase_percentage: { api_field: 'annual_increase_percentage' },
withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' },
withdrawal_period_specified: { api_field: 'withdrawal_period' },
withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' },
withdrawal_period_fixed: { api_field: 'withdrawal_period' },
total_amount: { api_field: 'total_premium', transform: 'fen_to_yuan' }
}
......@@ -293,6 +296,7 @@ const submit = async () => {
// 映射表单字段到 API 字段
const submitMapping = templateConfig.value?.config?.submit_mapping || defaultMapping
const { toYuan } = useFieldValueTransform(formData, submitMapping)
Object.keys(formData.value).forEach(key => {
const mapping = submitMapping[key]
......@@ -301,7 +305,7 @@ const submit = async () => {
let value = formData.value[key]
// 金额字段从分转换为元
if (typeof mapping === 'object' && mapping.transform === 'fen_to_yuan' && value !== null && value !== undefined && value !== '') {
value = (value / 100).toFixed(2)
value = toYuan(key, value)
}
requestData[apiField] = value
} else {
......@@ -309,6 +313,28 @@ const submit = async () => {
}
})
if (formData.value?.withdrawal_mode === '指定提取金额') {
const specifiedStart = formData.value.withdrawal_start_age_specified
const specifiedPeriod = formData.value.withdrawal_period_specified
if (specifiedStart !== undefined && specifiedStart !== null && specifiedStart !== '') {
requestData.withdrawal_start_age = specifiedStart
}
if (specifiedPeriod !== undefined && specifiedPeriod !== null && specifiedPeriod !== '') {
requestData.withdrawal_period = specifiedPeriod
}
}
if (formData.value?.withdrawal_mode === '最高固定提取金额') {
const fixedStart = formData.value.withdrawal_start_age_fixed
const fixedPeriod = formData.value.withdrawal_period_fixed
if (fixedStart !== undefined && fixedStart !== null && fixedStart !== '') {
requestData.withdrawal_start_age = fixedStart
}
if (fixedPeriod !== undefined && fixedPeriod !== null && fixedPeriod !== '') {
requestData.withdrawal_period = fixedPeriod
}
}
// 添加币种类型(如果有配置)
if (templateConfig.value?.config?.currency) {
requestData.currency_type = templateConfig.value.config.currency
......
......@@ -2,13 +2,13 @@
<div v-if="config">
<template v-for="field in baseFields" :key="field.id || field.key">
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
......@@ -51,6 +51,7 @@ import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
import { useFieldDependencies } from '@/composables/useFieldDependencies'
/**
* 组件属性
......@@ -117,6 +118,13 @@ const fieldComponentMap = {
// Schema 配置入口
const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
const fieldDefinitions = computed(() => {
return baseFields.value.reduce((result, field) => {
result[field.key] = field
return result
}, {})
})
/**
* 获取字段对应的渲染组件
* @param {Object} field - 字段配置
......@@ -160,20 +168,7 @@ const getFieldProps = (field) => {
return fieldProps
}
/**
* 判断字段是否可见
* @param {Object} field - 字段配置
* @returns {boolean} 是否显示
*/
const isFieldVisible = (field) => {
if (!field.show_when || field.show_when.length === 0) {
return true
}
return field.show_when.every(condition => {
return form[condition.field] === condition.equals
})
}
const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
/**
* 获取 Schema 默认值
......@@ -296,7 +291,7 @@ const validate = () => {
const fields = [...baseFields.value]
for (const field of fields) {
if (!isFieldVisible(field)) {
if (!isFieldVisible(field.key)) {
continue
}
......@@ -308,7 +303,7 @@ const validate = () => {
}
}
if (field.type === 'percentage' && isFieldVisible(field)) {
if (field.type === 'percentage' && isFieldVisible(field.key)) {
const percentage = parseFloat(form[field.key])
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
......
......@@ -2,13 +2,13 @@
<div v-if="config">
<template v-for="field in baseFields" :key="field.id || field.key">
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
......@@ -51,6 +51,7 @@ import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
import { useFieldDependencies } from '@/composables/useFieldDependencies'
/**
* 组件属性
......@@ -119,6 +120,13 @@ const fieldComponentMap = {
// Schema 配置入口
const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
const fieldDefinitions = computed(() => {
return baseFields.value.reduce((result, field) => {
result[field.key] = field
return result
}, {})
})
/**
* 获取字段对应的渲染组件
* @param {Object} field - 字段配置
......@@ -162,20 +170,7 @@ const getFieldProps = (field) => {
return fieldProps
}
/**
* 判断字段是否可见
* @param {Object} field - 字段配置
* @returns {boolean} 是否显示
*/
const isFieldVisible = (field) => {
if (!field.show_when || field.show_when.length === 0) {
return true
}
return field.show_when.every(condition => {
return form[condition.field] === condition.equals
})
}
const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
/**
* 获取 Schema 默认值
......@@ -298,7 +293,7 @@ const validate = () => {
const fields = [...baseFields.value]
for (const field of fields) {
if (!isFieldVisible(field)) {
if (!isFieldVisible(field.key)) {
continue
}
......@@ -310,7 +305,7 @@ const validate = () => {
}
}
if (field.type === 'percentage' && isFieldVisible(field)) {
if (field.type === 'percentage' && isFieldVisible(field.key)) {
const percentage = parseFloat(form[field.key])
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
......
......@@ -2,13 +2,13 @@
<div v-if="config">
<template v-for="field in baseFields" :key="field.id || field.key">
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
......@@ -31,13 +31,13 @@
{{ field.section_title }}
</h3>
<component
v-if="isFieldVisible(field) && field.type !== 'percentage'"
v-if="isFieldVisible(field.key) && field.type !== 'percentage'"
:is="getFieldComponent(field)"
v-model="form[field.key]"
v-bind="getFieldProps(field)"
class="mb-5"
/>
<div v-else-if="isFieldVisible(field) && field.type === 'percentage'" class="mb-5">
<div v-else-if="isFieldVisible(field.key) && field.type === 'percentage'" class="mb-5">
<div class="text-sm text-gray-700 mb-2 flex items-center">
<span v-if="field.required" class="text-red-500 mr-1">*</span>
<span>{{ field.label }}</span>
......@@ -84,6 +84,7 @@ import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
import { useFieldDependencies } from '@/composables/useFieldDependencies'
/**
* 组件属性
......@@ -161,6 +162,13 @@ const baseFields = computed(() => props.config?.form_schema?.base_fields || [])
const withdrawalFields = computed(() => props.config?.form_schema?.withdrawal_fields || [])
const resetMap = computed(() => props.config?.form_schema?.reset_map || {})
const fieldDefinitions = computed(() => {
return [...baseFields.value, ...withdrawalFields.value].reduce((result, field) => {
result[field.key] = field
return result
}, {})
})
/**
* 获取字段对应的渲染组件
* @param {Object} field - 字段配置
......@@ -214,20 +222,7 @@ const getFieldProps = (field) => {
return fieldProps
}
/**
* 判断字段是否可见
* @param {Object} field - 字段配置
* @returns {boolean} 是否显示
*/
const isFieldVisible = (field) => {
if (!field.show_when || field.show_when.length === 0) {
return true
}
return field.show_when.every(condition => {
return form[condition.field] === condition.equals
})
}
const { isFieldVisible } = useFieldDependencies(form, fieldDefinitions)
/**
* 获取 Schema 默认值
......@@ -261,7 +256,11 @@ const initializeForm = (value) => {
...value,
...defaults,
annual_withdrawal_amount: value.annual_withdrawal_amount ?? null,
annual_increase_percentage: value.annual_increase_percentage ?? null
annual_increase_percentage: value.annual_increase_percentage ?? null,
withdrawal_start_age_specified: value.withdrawal_start_age_specified ?? null,
withdrawal_period_specified: value.withdrawal_period_specified ?? null,
withdrawal_start_age_fixed: value.withdrawal_start_age_fixed ?? null,
withdrawal_period_fixed: value.withdrawal_period_fixed ?? null
})
}
......@@ -292,7 +291,11 @@ watch(
...newVal,
...defaults,
annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null,
annual_increase_percentage: newVal.annual_increase_percentage ?? null
annual_increase_percentage: newVal.annual_increase_percentage ?? null,
withdrawal_start_age_specified: newVal.withdrawal_start_age_specified ?? null,
withdrawal_period_specified: newVal.withdrawal_period_specified ?? null,
withdrawal_start_age_fixed: newVal.withdrawal_start_age_fixed ?? null,
withdrawal_period_fixed: newVal.withdrawal_period_fixed ?? null
})
previousModelValue = newVal
}
......@@ -386,7 +389,7 @@ const validate = () => {
const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])]
for (const field of fields) {
if (!isFieldVisible(field)) {
if (!isFieldVisible(field.key)) {
continue
}
......@@ -398,7 +401,7 @@ const validate = () => {
}
}
if (field.type === 'percentage' && isFieldVisible(field)) {
if (field.type === 'percentage' && isFieldVisible(field.key)) {
const percentage = parseFloat(form[field.key])
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
......
......@@ -7,7 +7,7 @@
* @created 2026-02-14
*/
import { computed, reactive } from 'vue'
import { computed, reactive, isRef } from 'vue'
import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
/**
......@@ -24,7 +24,7 @@ import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
* detectCircularDeps('B') // true
* detectCircularDeps('C') // true
*/
function detectCircularDeps(fieldKey, visited = new Set()) {
function detectCircularDeps(fieldKey, fieldDefinitions, visited = new Set()) {
// 防止无限递归
if (visited.size > 50) {
console.error('[useFieldDependencies] 依赖层级过深,可能存在循环依赖')
......@@ -38,12 +38,12 @@ function detectCircularDeps(fieldKey, visited = new Set()) {
}
visited.add(fieldKey)
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
const definition = fieldDefinitions[fieldKey]
if (!definition?.affects) return false
// 递归检查依赖字段
for (const depKey of definition.affects) {
if (detectCircularDeps(depKey, visited)) {
if (detectCircularDeps(depKey, fieldDefinitions, visited)) {
return true
}
}
......@@ -73,13 +73,18 @@ function detectCircularDeps(fieldKey, visited = new Set()) {
* // 获取所有可见字段
* const visible = visibleFields.value
*/
export function useFieldDependencies(formData) {
export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
// 字段显示状态映射
const fieldVisibility = reactive({})
// 字段启用状态映射
const fieldEnabled = reactive({})
const getFieldDefinitions = () => {
const definitions = isRef(fieldDefinitions) ? fieldDefinitions.value : fieldDefinitions
return definitions || {}
}
/**
* 检查字段是否应该显示
*
......@@ -87,22 +92,33 @@ export function useFieldDependencies(formData) {
* @returns {boolean} 是否显示
*/
function isFieldVisible(fieldKey) {
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
const definitions = getFieldDefinitions()
const definition = definitions[fieldKey]
if (!definition) return false
// 检查是否有 show_when 条件
if (definition.show_when) {
const conditions = definition.show_when
for (const [depKey, expectedValue] of Object.entries(conditions)) {
const currentValue = formData[depKey]
if (currentValue !== expectedValue) {
return false
if (Array.isArray(conditions)) {
for (const condition of conditions) {
if (!condition) continue
const currentValue = formData[condition.field]
if (currentValue !== condition.equals) {
return false
}
}
} else if (conditions && typeof conditions === 'object') {
for (const [depKey, expectedValue] of Object.entries(conditions)) {
const currentValue = formData[depKey]
if (currentValue !== expectedValue) {
return false
}
}
}
}
// 检查是否被依赖字段影响
for (const [key, def] of Object.entries(PLAN_FIELD_DEFINITIONS)) {
for (const [key, def] of Object.entries(definitions)) {
if (def.affects?.includes(fieldKey)) {
// 依赖字段必须为 true 才显示
if (formData[key] !== true) {
......@@ -121,7 +137,7 @@ export function useFieldDependencies(formData) {
* @returns {boolean} 是否启用
*/
function isFieldEnabled(fieldKey) {
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
const definition = getFieldDefinitions()[fieldKey]
if (!definition) return false
// 如果有依赖字段,检查依赖字段是否满足
......@@ -143,7 +159,7 @@ export function useFieldDependencies(formData) {
formData[fieldKey] = value
// 更新受影响字段的显示状态
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
const definition = getFieldDefinitions()[fieldKey]
if (definition?.affects) {
for (const affectedKey of definition.affects) {
fieldVisibility[affectedKey] = isFieldVisible(affectedKey)
......@@ -158,21 +174,22 @@ export function useFieldDependencies(formData) {
* @returns {string[]} 可见字段键名数组
*/
const visibleFields = computed(() => {
return Object.keys(PLAN_FIELD_DEFINITIONS).filter(key => isFieldVisible(key))
return Object.keys(getFieldDefinitions()).filter(key => isFieldVisible(key))
})
/**
* 初始化所有字段的显示状态(包含循环依赖检测)
*/
function initFieldStates() {
const definitions = getFieldDefinitions()
// 开发环境检测循环依赖
if (process.env.NODE_ENV === 'development') {
for (const key of Object.keys(PLAN_FIELD_DEFINITIONS)) {
detectCircularDeps(key)
for (const key of Object.keys(definitions)) {
detectCircularDeps(key, definitions)
}
}
for (const key of Object.keys(PLAN_FIELD_DEFINITIONS)) {
for (const key of Object.keys(definitions)) {
fieldVisibility[key] = isFieldVisible(key)
fieldEnabled[key] = isFieldEnabled(key)
}
......
......@@ -8,7 +8,7 @@
* @version 1.1.0 - 简化转换逻辑,减少重复代码
*/
import { computed } from 'vue'
import { computed, isRef } from 'vue'
import { PLAN_FIELD_DEFINITIONS, TRANSFORM_TYPES } from '@/config/plan-fields'
import { transformFieldValue, batchTransformFields } from '@/utils/planFieldTransformers'
......@@ -29,7 +29,12 @@ import { transformFieldValue, batchTransformFields } from '@/utils/planFieldTran
* toFen('coverage', '100.00') // 10000
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
export function useFieldValueTransform(formData) {
export function useFieldValueTransform(formData, fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
const getFieldDefinitions = () => {
const definitions = isRef(fieldDefinitions) ? fieldDefinitions.value : fieldDefinitions
return definitions || {}
}
const getReverseTransform = (transform) => {
if (!transform || transform === TRANSFORM_TYPES.NONE) return TRANSFORM_TYPES.NONE
if (transform === TRANSFORM_TYPES.FEN_TO_YUAN) return TRANSFORM_TYPES.YUAN_TO_FEN
......@@ -37,14 +42,20 @@ export function useFieldValueTransform(formData) {
return TRANSFORM_TYPES.NONE
}
const reverseFieldDefinitions = Object.entries(PLAN_FIELD_DEFINITIONS).reduce((result, [key, definition]) => {
const reverseTransform = getReverseTransform(definition.transform)
result[key] = {
...definition,
transform: reverseTransform
}
return result
}, {})
const getReverseFieldDefinitions = () => {
return Object.entries(getFieldDefinitions()).reduce((result, [key, definition]) => {
if (!definition || typeof definition === 'string') {
result[key] = { transform: TRANSFORM_TYPES.NONE }
return result
}
const reverseTransform = getReverseTransform(definition.transform)
result[key] = {
...definition,
transform: reverseTransform
}
return result
}, {})
}
/**
* 转换为分值(用于显示)
......@@ -61,8 +72,8 @@ export function useFieldValueTransform(formData) {
const toYuan = (fieldKey, value) => {
if (value === undefined) return undefined
if (value === null) return null
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
if (!definition) return value
const definition = getFieldDefinitions()[fieldKey]
if (!definition || typeof definition === 'string') return value
if (!definition.transform || definition.transform === TRANSFORM_TYPES.NONE) {
return value
......@@ -87,8 +98,8 @@ export function useFieldValueTransform(formData) {
const toFen = (fieldKey, value) => {
if (value === undefined) return undefined
if (value === null) return null
const definition = PLAN_FIELD_DEFINITIONS[fieldKey]
if (!definition) return value
const definition = getFieldDefinitions()[fieldKey]
if (!definition || typeof definition === 'string') return value
const reverseTransform = getReverseTransform(definition.transform)
if (!reverseTransform || reverseTransform === TRANSFORM_TYPES.NONE) {
......@@ -110,7 +121,7 @@ export function useFieldValueTransform(formData) {
* // { coverage: '100.00', name: 'Test' }
*/
const batchToYuan = (sourceData) => {
return batchTransformFields(sourceData, PLAN_FIELD_DEFINITIONS)
return batchTransformFields(sourceData, getFieldDefinitions())
}
/**
......@@ -125,7 +136,7 @@ export function useFieldValueTransform(formData) {
* // { coverage: 10000, name: 'Test' }
*/
const batchToFen = (yuanData) => {
return batchTransformFields(yuanData, reverseFieldDefinitions)
return batchTransformFields(yuanData, getReverseFieldDefinitions())
}
// 计算属性:表单显示数据(元值转分值显示)
......
......@@ -47,11 +47,13 @@ const savingsSubmitMapping = {
...baseSubmitMapping,
withdrawal_enabled: { api_field: 'allow_reduce_amount' },
withdrawal_mode: { api_field: 'withdrawal_option' },
withdrawal_start_age: { api_field: 'withdrawal_start_age' },
withdrawal_period: { api_field: 'withdrawal_period' },
withdrawal_method: { api_field: 'withdrawal_method' },
annual_withdrawal_amount: { api_field: 'annual_withdrawal_amount', transform: 'fen_to_yuan' },
annual_increase_percentage: { api_field: 'annual_increase_percentage' }
annual_increase_percentage: { api_field: 'annual_increase_percentage' },
withdrawal_start_age_specified: { api_field: 'withdrawal_start_age' },
withdrawal_period_specified: { api_field: 'withdrawal_period' },
withdrawal_start_age_fixed: { api_field: 'withdrawal_start_age' },
withdrawal_period_fixed: { api_field: 'withdrawal_period' }
}
// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口)
......@@ -70,18 +72,18 @@ const savingsFormSchema = {
{ id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' },
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' },
{ id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ 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: '指定提取金额' }, { field: 'withdrawal_method', equals: '按年岁' }] },
{ id: 'withdrawal_start_age_by_year', key: 'withdrawal_start_age', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_period_by_year', key: 'withdrawal_period', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }, { field: 'withdrawal_method', equals: '按年岁' }] },
{ id: 'withdrawal_start_age_by_fixed', key: 'withdrawal_start_age', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
{ id: 'withdrawal_period_by_fixed', key: 'withdrawal_period', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }
{ 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: '指定提取金额' }] },
{ id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ 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: '指定提取金额' }] },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
{ 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: '最高固定提取金额' }] }
],
// 提取模式切换时的清空逻辑,避免脏字段影响提交
reset_map: {
withdrawal_mode: {
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age', 'withdrawal_period'],
'指定提取金额': ['withdrawal_start_age', 'withdrawal_period']
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'],
'指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed']
}
}
}
......