feat(plan): 年龄与出生年月日改为二选一填写
实现方案B:两个字段都显示,用户可任选其一填写 - 填年龄 → 自动推算生日(默认当年1月1日) - 填生日 → 自动计算年龄 - 两个都填 → 以生日为准 - 两个都不填 → 校验提示"至少填写一项" 覆盖所有计划书模板: - LifeInsuranceTemplate(人寿保险) - CriticalIllnessTemplate(重疾保险) - SavingsTemplate(储蓄型产品) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
4 changed files
with
153 additions
and
2 deletions
| ... | @@ -49,6 +49,7 @@ import Taro from '@tarojs/taro' | ... | @@ -49,6 +49,7 @@ import Taro from '@tarojs/taro' |
| 49 | import PlanFieldName from '../PlanFields/NameInput.vue' | 49 | import PlanFieldName from '../PlanFields/NameInput.vue' |
| 50 | import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' | 50 | import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' |
| 51 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' | 51 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' |
| 52 | +import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | ||
| 52 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | 53 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' |
| 53 | import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' | 54 | import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' |
| 54 | import { useFieldDependencies } from '@/composables/useFieldDependencies' | 55 | import { useFieldDependencies } from '@/composables/useFieldDependencies' |
| ... | @@ -111,6 +112,7 @@ const fieldComponentMap = { | ... | @@ -111,6 +112,7 @@ const fieldComponentMap = { |
| 111 | name: PlanFieldName, | 112 | name: PlanFieldName, |
| 112 | radio: PlanFieldRadio, | 113 | radio: PlanFieldRadio, |
| 113 | date: PlanFieldDatePicker, | 114 | date: PlanFieldDatePicker, |
| 115 | + age: PlanFieldAgePicker, | ||
| 114 | amount: PlanFieldAmount, | 116 | amount: PlanFieldAmount, |
| 115 | payment_period: PaymentPeriodRadio | 117 | payment_period: PaymentPeriodRadio |
| 116 | } | 118 | } |
| ... | @@ -248,6 +250,33 @@ watch( | ... | @@ -248,6 +250,33 @@ watch( |
| 248 | ) | 250 | ) |
| 249 | 251 | ||
| 250 | /** | 252 | /** |
| 253 | + * 年龄与出生年月日自动计算逻辑 | ||
| 254 | + * - 填年龄 → 推算生日(默认当年1月1日) | ||
| 255 | + * - 填生日 → 计算年龄 | ||
| 256 | + */ | ||
| 257 | +watch( | ||
| 258 | + () => form.age, | ||
| 259 | + (newAge) => { | ||
| 260 | + if (!isEmptyValue(newAge) && isEmptyValue(form.birthday)) { | ||
| 261 | + const currentYear = new Date().getFullYear() | ||
| 262 | + const birthYear = currentYear - parseInt(newAge) | ||
| 263 | + form.birthday = `${birthYear}-01-01` | ||
| 264 | + } | ||
| 265 | + } | ||
| 266 | +) | ||
| 267 | + | ||
| 268 | +watch( | ||
| 269 | + () => form.birthday, | ||
| 270 | + (newBirthday) => { | ||
| 271 | + if (!isEmptyValue(newBirthday)) { | ||
| 272 | + const birthYear = new Date(newBirthday).getFullYear() | ||
| 273 | + const currentYear = new Date().getFullYear() | ||
| 274 | + form.age = currentYear - birthYear | ||
| 275 | + } | ||
| 276 | + } | ||
| 277 | +) | ||
| 278 | + | ||
| 279 | +/** | ||
| 251 | * 百分比输入清洗,避免非法字符 | 280 | * 百分比输入清洗,避免非法字符 |
| 252 | * @param {string|number} value - 输入值 | 281 | * @param {string|number} value - 输入值 |
| 253 | * @param {string} key - 目标字段 key | 282 | * @param {string} key - 目标字段 key |
| ... | @@ -311,11 +340,30 @@ const isFieldRequired = (field) => { | ... | @@ -311,11 +340,30 @@ const isFieldRequired = (field) => { |
| 311 | const validate = () => { | 340 | const validate = () => { |
| 312 | const fields = [...baseFields.value] | 341 | const fields = [...baseFields.value] |
| 313 | 342 | ||
| 343 | + // 年龄与出生年月日二选一校验 | ||
| 344 | + const hasAge = !isEmptyValue(form.age) | ||
| 345 | + const hasBirthday = !isEmptyValue(form.birthday) | ||
| 346 | + | ||
| 347 | + if (!hasAge && !hasBirthday) { | ||
| 348 | + Taro.showToast({ title: '年龄与出生年月日至少填写一项', icon: 'none' }) | ||
| 349 | + return false | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + // 如果都填写了,以生日为准,重新计算年龄 | ||
| 353 | + if (hasAge && hasBirthday) { | ||
| 354 | + const birthYear = new Date(form.birthday).getFullYear() | ||
| 355 | + const currentYear = new Date().getFullYear() | ||
| 356 | + form.age = currentYear - birthYear | ||
| 357 | + } | ||
| 358 | + | ||
| 314 | for (const field of fields) { | 359 | for (const field of fields) { |
| 315 | if (!isFieldVisible(field.key)) { | 360 | if (!isFieldVisible(field.key)) { |
| 316 | continue | 361 | continue |
| 317 | } | 362 | } |
| 318 | 363 | ||
| 364 | + // 跳过年龄字段的单独校验(已和生日一起校验) | ||
| 365 | + if (field.key === 'age') continue | ||
| 366 | + | ||
| 319 | if (isFieldRequired(field)) { | 367 | if (isFieldRequired(field)) { |
| 320 | const value = form[field.key] | 368 | const value = form[field.key] |
| 321 | if (isEmptyValue(value)) { | 369 | if (isEmptyValue(value)) { | ... | ... |
| ... | @@ -49,6 +49,7 @@ import Taro from '@tarojs/taro' | ... | @@ -49,6 +49,7 @@ import Taro from '@tarojs/taro' |
| 49 | import PlanFieldName from '../PlanFields/NameInput.vue' | 49 | import PlanFieldName from '../PlanFields/NameInput.vue' |
| 50 | import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' | 50 | import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' |
| 51 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' | 51 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' |
| 52 | +import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | ||
| 52 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | 53 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' |
| 53 | import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' | 54 | import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' |
| 54 | import { useFieldDependencies } from '@/composables/useFieldDependencies' | 55 | import { useFieldDependencies } from '@/composables/useFieldDependencies' |
| ... | @@ -113,6 +114,7 @@ const fieldComponentMap = { | ... | @@ -113,6 +114,7 @@ const fieldComponentMap = { |
| 113 | name: PlanFieldName, | 114 | name: PlanFieldName, |
| 114 | radio: PlanFieldRadio, | 115 | radio: PlanFieldRadio, |
| 115 | date: PlanFieldDatePicker, | 116 | date: PlanFieldDatePicker, |
| 117 | + age: PlanFieldAgePicker, | ||
| 116 | amount: PlanFieldAmount, | 118 | amount: PlanFieldAmount, |
| 117 | payment_period: PaymentPeriodRadio | 119 | payment_period: PaymentPeriodRadio |
| 118 | } | 120 | } |
| ... | @@ -250,6 +252,35 @@ watch( | ... | @@ -250,6 +252,35 @@ watch( |
| 250 | ) | 252 | ) |
| 251 | 253 | ||
| 252 | /** | 254 | /** |
| 255 | + * 年龄与出生年月日自动计算逻辑 | ||
| 256 | + * - 填年龄 → 推算生日(默认当年1月1日) | ||
| 257 | + * - 填生日 → 计算年龄 | ||
| 258 | + */ | ||
| 259 | +watch( | ||
| 260 | + () => form.age, | ||
| 261 | + (newAge) => { | ||
| 262 | + if (!isEmptyValue(newAge) && isEmptyValue(form.birthday)) { | ||
| 263 | + // 填了年龄,推算生日(默认当年1月1日) | ||
| 264 | + const currentYear = new Date().getFullYear() | ||
| 265 | + const birthYear = currentYear - parseInt(newAge) | ||
| 266 | + form.birthday = `${birthYear}-01-01` | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | +) | ||
| 270 | + | ||
| 271 | +watch( | ||
| 272 | + () => form.birthday, | ||
| 273 | + (newBirthday) => { | ||
| 274 | + if (!isEmptyValue(newBirthday)) { | ||
| 275 | + // 填了生日,计算年龄 | ||
| 276 | + const birthYear = new Date(newBirthday).getFullYear() | ||
| 277 | + const currentYear = new Date().getFullYear() | ||
| 278 | + form.age = currentYear - birthYear | ||
| 279 | + } | ||
| 280 | + } | ||
| 281 | +) | ||
| 282 | + | ||
| 283 | +/** | ||
| 253 | * 百分比输入清洗,避免非法字符 | 284 | * 百分比输入清洗,避免非法字符 |
| 254 | * @param {string|number} value - 输入值 | 285 | * @param {string|number} value - 输入值 |
| 255 | * @param {string} key - 目标字段 key | 286 | * @param {string} key - 目标字段 key |
| ... | @@ -313,11 +344,31 @@ const isFieldRequired = (field) => { | ... | @@ -313,11 +344,31 @@ const isFieldRequired = (field) => { |
| 313 | const validate = () => { | 344 | const validate = () => { |
| 314 | const fields = [...baseFields.value] | 345 | const fields = [...baseFields.value] |
| 315 | 346 | ||
| 347 | + // 年龄与出生年月日二选一校验 | ||
| 348 | + const hasAge = !isEmptyValue(form.age) | ||
| 349 | + const hasBirthday = !isEmptyValue(form.birthday) | ||
| 350 | + | ||
| 351 | + if (!hasAge && !hasBirthday) { | ||
| 352 | + Taro.showToast({ title: '年龄与出生年月日至少填写一项', icon: 'none' }) | ||
| 353 | + return false | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + // 如果都填写了,以生日为准,重新计算年龄 | ||
| 357 | + if (hasAge && hasBirthday) { | ||
| 358 | + // 使用生日重新计算年龄 | ||
| 359 | + const birthYear = new Date(form.birthday).getFullYear() | ||
| 360 | + const currentYear = new Date().getFullYear() | ||
| 361 | + form.age = String(currentYear - birthYear) | ||
| 362 | + } | ||
| 363 | + | ||
| 316 | for (const field of fields) { | 364 | for (const field of fields) { |
| 317 | if (!isFieldVisible(field.key)) { | 365 | if (!isFieldVisible(field.key)) { |
| 318 | continue | 366 | continue |
| 319 | } | 367 | } |
| 320 | 368 | ||
| 369 | + // 跳过年龄字段的单独校验(已和生日一起校验) | ||
| 370 | + if (field.key === 'age') continue | ||
| 371 | + | ||
| 321 | if (isFieldRequired(field)) { | 372 | if (isFieldRequired(field)) { |
| 322 | const value = form[field.key] | 373 | const value = form[field.key] |
| 323 | if (isEmptyValue(value)) { | 374 | if (isEmptyValue(value)) { | ... | ... |
| ... | @@ -332,6 +332,33 @@ watch( | ... | @@ -332,6 +332,33 @@ watch( |
| 332 | ) | 332 | ) |
| 333 | 333 | ||
| 334 | /** | 334 | /** |
| 335 | + * 年龄与出生年月日自动计算逻辑 | ||
| 336 | + * - 填年龄 → 推算生日(默认当年1月1日) | ||
| 337 | + * - 填生日 → 计算年龄 | ||
| 338 | + */ | ||
| 339 | +watch( | ||
| 340 | + () => form.age, | ||
| 341 | + (newAge) => { | ||
| 342 | + if (!isEmptyValue(newAge) && isEmptyValue(form.birthday)) { | ||
| 343 | + const currentYear = new Date().getFullYear() | ||
| 344 | + const birthYear = currentYear - parseInt(newAge) | ||
| 345 | + form.birthday = `${birthYear}-01-01` | ||
| 346 | + } | ||
| 347 | + } | ||
| 348 | +) | ||
| 349 | + | ||
| 350 | +watch( | ||
| 351 | + () => form.birthday, | ||
| 352 | + (newBirthday) => { | ||
| 353 | + if (!isEmptyValue(newBirthday)) { | ||
| 354 | + const birthYear = new Date(newBirthday).getFullYear() | ||
| 355 | + const currentYear = new Date().getFullYear() | ||
| 356 | + form.age = currentYear - birthYear | ||
| 357 | + } | ||
| 358 | + } | ||
| 359 | +) | ||
| 360 | + | ||
| 361 | +/** | ||
| 335 | * 提取年期选项(从配置读取) | 362 | * 提取年期选项(从配置读取) |
| 336 | * @type {ComputedRef<Array<string>>} | 363 | * @type {ComputedRef<Array<string>>} |
| 337 | */ | 364 | */ |
| ... | @@ -409,11 +436,30 @@ const isFieldRequired = (field) => { | ... | @@ -409,11 +436,30 @@ const isFieldRequired = (field) => { |
| 409 | const validate = () => { | 436 | const validate = () => { |
| 410 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] | 437 | const fields = [...baseFields.value, ...(props.config.withdrawal_plan?.enabled ? withdrawalFields.value : [])] |
| 411 | 438 | ||
| 439 | + // 年龄与出生年月日二选一校验 | ||
| 440 | + const hasAge = !isEmptyValue(form.age) | ||
| 441 | + const hasBirthday = !isEmptyValue(form.birthday) | ||
| 442 | + | ||
| 443 | + if (!hasAge && !hasBirthday) { | ||
| 444 | + Taro.showToast({ title: '年龄与出生年月日至少填写一项', icon: 'none' }) | ||
| 445 | + return false | ||
| 446 | + } | ||
| 447 | + | ||
| 448 | + // 如果都填写了,以生日为准,重新计算年龄 | ||
| 449 | + if (hasAge && hasBirthday) { | ||
| 450 | + const birthYear = new Date(form.birthday).getFullYear() | ||
| 451 | + const currentYear = new Date().getFullYear() | ||
| 452 | + form.age = currentYear - birthYear | ||
| 453 | + } | ||
| 454 | + | ||
| 412 | for (const field of fields) { | 455 | for (const field of fields) { |
| 413 | if (!isFieldVisible(field.key)) { | 456 | if (!isFieldVisible(field.key)) { |
| 414 | continue | 457 | continue |
| 415 | } | 458 | } |
| 416 | 459 | ||
| 460 | + // 跳过年龄字段的单独校验(已和生日一起校验) | ||
| 461 | + if (field.key === 'age') continue | ||
| 462 | + | ||
| 417 | if (isFieldRequired(field)) { | 463 | if (isFieldRequired(field)) { |
| 418 | const value = form[field.key] | 464 | const value = form[field.key] |
| 419 | if (isEmptyValue(value)) { | 465 | if (isEmptyValue(value)) { | ... | ... |
| ... | @@ -38,6 +38,7 @@ | ... | @@ -38,6 +38,7 @@ |
| 38 | const baseSubmitMapping = { | 38 | const baseSubmitMapping = { |
| 39 | customer_name: { api_field: 'customer_name' }, | 39 | customer_name: { api_field: 'customer_name' }, |
| 40 | gender: { api_field: 'customer_gender' }, | 40 | gender: { api_field: 'customer_gender' }, |
| 41 | + age: { api_field: 'customer_age' }, | ||
| 41 | birthday: { api_field: 'customer_birthday' }, | 42 | birthday: { api_field: 'customer_birthday' }, |
| 42 | smoker: { api_field: 'smoking_status' }, | 43 | smoker: { api_field: 'smoking_status' }, |
| 43 | coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, | 44 | coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' }, |
| ... | @@ -50,7 +51,9 @@ const protectionFormSchema = { | ... | @@ -50,7 +51,9 @@ const protectionFormSchema = { |
| 50 | base_fields: [ | 51 | base_fields: [ |
| 51 | { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | 52 | { 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: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, |
| 53 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | 54 | + // 年龄与出生年月日二选一填写 |
| 55 | + { id: 'age', key: 'age', type: 'age', label: '年龄', placeholder: '请输入年龄', input_label: '岁', required: false }, | ||
| 56 | + { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: false }, | ||
| 54 | { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | 57 | { 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' }, | 58 | { 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' } | 59 | { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } |
| ... | @@ -73,12 +76,15 @@ const savingsSubmitMapping = { | ... | @@ -73,12 +76,15 @@ const savingsSubmitMapping = { |
| 73 | 76 | ||
| 74 | // 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) | 77 | // 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口) |
| 75 | // @updated 2026-02-15 - 迁移到新的条件规则格式,使用 clear_when_hidden 替代 reset_map | 78 | // @updated 2026-02-15 - 迁移到新的条件规则格式,使用 clear_when_hidden 替代 reset_map |
| 79 | +// @updated 2026-02-25 - 年龄与出生年月日二选一填写 | ||
| 76 | const savingsFormSchema = { | 80 | const savingsFormSchema = { |
| 77 | // 基础字段:非提取计划部分 | 81 | // 基础字段:非提取计划部分 |
| 78 | base_fields: [ | 82 | base_fields: [ |
| 79 | { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, | 83 | { id: 'customer_name', key: 'customer_name', type: 'name', label: '申请人', placeholder: '请输入申请人', required: true }, |
| 80 | { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, | 84 | { id: 'gender', key: 'gender', type: 'radio', label: '性别', options: ['男', '女'], required: true }, |
| 81 | - { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: true }, | 85 | + // 年龄与出生年月日二选一填写 |
| 86 | + { id: 'age', key: 'age', type: 'age', label: '年龄', placeholder: '请输入年龄', input_label: '岁', required: false }, | ||
| 87 | + { id: 'birthday', key: 'birthday', type: 'date', label: '出生年月日', placeholder: '请选择年月日', required: false }, | ||
| 82 | { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, | 88 | { id: 'smoker', key: 'smoker', type: 'radio', label: '是否吸烟', options: ['是', '否'], required: true }, |
| 83 | { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, | 89 | { id: 'coverage', key: 'coverage', type: 'amount', label: '年缴保费', placeholder: '请输入年缴保费', input_label: '请输入年缴保费金额', required: true, currency_from: 'currency' }, |
| 84 | { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | 90 | { id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' } | ... | ... |
-
Please register or login to post a comment