hookehuyr

feat(plan): 支持MPC孕22周年龄选项

......@@ -13,7 +13,7 @@ const Api = {
* @param {Object} params 请求参数
* @param {string} params.customer_name 申请人
* @param {string} params.customer_gender 性别
* @param {integer} params.customer_age 年龄
* @param {string} params.customer_age 年龄
* @param {string} params.customer_birthday 出生年月日
* @param {integer} params.annual_premium 年缴保费
* @param {string} params.payment_years 繳費年期
......
......@@ -43,6 +43,7 @@
* - 显示格式:3位数字(如 018 表示 18 岁)
* - 提交格式:数字(如 18)
* - 年龄范围:0-120 岁
* - 支持按产品配置注入特殊年龄选项(如“孕22周”)
* - 使用 GlobalPopupManager 管理弹窗层级
* @author Claude Code
* @version 2.0.0 - 支持全局弹窗管理器
......@@ -56,6 +57,13 @@
import { ref, computed, watch, onMounted } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
import { useGlobalPopup } from './GlobalPopupManager'
import {
buildAgePickerColumn,
DEFAULT_AGE_PICKER_VALUE,
formatAgeDisplayValue,
normalizeAgePickerValue,
parseAgePickerValue
} from '@/utils/agePickerOptions'
/**
* 使用全局弹窗管理器
......@@ -107,12 +115,21 @@ const props = defineProps({
},
/**
* 绑定的值(数字)
* @type {number}
* 绑定的值(数字年龄或特殊年龄文本
* @type {number|string}
*/
modelValue: {
type: Number,
type: [Number, String],
default: null
},
/**
* 产品级特殊年龄选项
* @type {Array<string>}
*/
specialOptions: {
type: Array,
default: () => []
}
})
......@@ -143,17 +160,14 @@ const showPicker = ref(false)
/**
* Picker 当前值(3位数字格式)
*/
const pickerValue = ref(['018'])
const pickerValue = ref([DEFAULT_AGE_PICKER_VALUE])
/**
* 年龄选项列(0-120 岁,3位数字格式)
*/
const ageColumns = computed(() => {
return [
Array.from({ length: 121 }, (_, i) => ({
text: `${i} 岁`,
value: String(i).padStart(3, '0')
}))
buildAgePickerColumn({ specialOptions: props.specialOptions })
]
})
......@@ -161,10 +175,7 @@ const ageColumns = computed(() => {
* 显示的值(转换为中文格式)
*/
const displayValue = computed(() => {
if (props.modelValue === null || props.modelValue === undefined) {
return ''
}
return `${props.modelValue} 岁`
return formatAgeDisplayValue(props.modelValue)
})
/**
......@@ -178,7 +189,7 @@ const handleTap = () => {
// 如果有值,转换为3位数字格式
if (props.modelValue !== null && props.modelValue !== undefined) {
pickerValue.value = [String(props.modelValue).padStart(3, '0')]
pickerValue.value = [normalizeAgePickerValue(props.modelValue, props.specialOptions)]
}
showPicker.value = true
......@@ -193,8 +204,7 @@ const handleTap = () => {
* onConfirm({ selectedValue: ['018'] })
*/
const onConfirm = ({ selectedValue }) => {
// 将3位数字格式转换为普通数字
const age = parseInt(selectedValue[0], 10)
const age = parseAgePickerValue(selectedValue[0])
emit('update:modelValue', age)
emit('change', age)
......@@ -226,7 +236,7 @@ watch(
() => props.modelValue,
(newVal) => {
if (newVal !== null && newVal !== undefined) {
pickerValue.value = [String(newVal).padStart(3, '0')]
pickerValue.value = [normalizeAgePickerValue(newVal, props.specialOptions)]
}
}
)
......
......@@ -72,6 +72,7 @@ const props = defineProps({
* @type {Object}
* @property {string} currency - 币种代码
* @property {Array<string>} payment_periods - 缴费年期选项
* @property {Array<string>} special_age_options - 产品专属年龄特殊选项
* @property {Object} age_range - 年龄范围 { min, max }
* @property {string} insurance_period - 保险期间
* @property {Object} form_schema - 表单 Schema
......@@ -152,6 +153,10 @@ const getFieldProps = (field) => {
fieldProps.options = field.options
}
if (field.key === 'age' && Array.isArray(props.config?.special_age_options)) {
fieldProps.specialOptions = props.config.special_age_options
}
// 缴费年期选项由模板配置提供
if (field.options_from === 'payment_periods') {
fieldProps.options = fieldProps.options || props.config?.payment_periods
......
......@@ -44,3 +44,11 @@ describe('plan field definitions amount semantics', () => {
})
})
})
describe('critical illness mpc special age options', () => {
it('should expose pregnancy week option only for mpc', () => {
expect(PLAN_TEMPLATES['critical-illness-mpc'].config.special_age_options).toEqual(['孕22周'])
expect(PLAN_TEMPLATES['critical-illness-mbc-pro'].config.special_age_options).toBeUndefined()
expect(PLAN_TEMPLATES['critical-illness-mbc2'].config.special_age_options).toBeUndefined()
})
})
......
......@@ -197,6 +197,7 @@ export const PLAN_TEMPLATES = {
component: 'CriticalIllnessTemplate',
config: {
currency: 'USD',
special_age_options: ['孕22周'], // 仅 MPC 使用:年龄字段增加前置特殊选项
payment_periods: [
'10 年(15 日 - 65 岁)',
'20 年(15 日 - 65 岁)',
......
import { describe, expect, it } from 'vitest'
import {
buildAgePickerColumn,
formatAgeDisplayValue,
normalizeAgePickerValue,
parseAgePickerValue,
SPECIAL_AGE_VALUE_PREFIX
} from '../agePickerOptions'
describe('agePickerOptions', () => {
it('should prepend special age options before numeric ages', () => {
const column = buildAgePickerColumn({ specialOptions: ['孕22周'] })
expect(column[0]).toEqual({
text: '孕22周',
value: `${SPECIAL_AGE_VALUE_PREFIX}孕22周`
})
expect(column[1]).toEqual({
text: '0 岁',
value: '000'
})
})
it('should format special and numeric age display values', () => {
expect(formatAgeDisplayValue('孕22周')).toBe('孕22周')
expect(formatAgeDisplayValue(0)).toBe('0 岁')
expect(formatAgeDisplayValue('12')).toBe('12 岁')
})
it('should normalize and parse picker values correctly', () => {
expect(normalizeAgePickerValue('孕22周', ['孕22周'])).toBe(`${SPECIAL_AGE_VALUE_PREFIX}孕22周`)
expect(normalizeAgePickerValue(3, ['孕22周'])).toBe('003')
expect(parseAgePickerValue(`${SPECIAL_AGE_VALUE_PREFIX}孕22周`)).toBe('孕22周')
expect(parseAgePickerValue('003')).toBe(3)
})
})
......@@ -72,6 +72,10 @@ describe('formatAge', () => {
expect(formatAge(0)).toBe('0岁')
})
it('should keep special age labels unchanged', () => {
expect(formatAge('孕22周')).toBe('孕22周')
})
it('should handle null and undefined', () => {
expect(formatAge(null)).toBe(null)
expect(formatAge(undefined)).toBe(null)
......
/**
* 特殊年龄选项在 Picker 内部使用字符串前缀编码,
* 用来和普通数字年龄值(如 "003")区分。
*/
export const SPECIAL_AGE_VALUE_PREFIX = '__special_age__:'
/**
* 未选择年龄时,Picker 默认定位到 18 岁。
*/
export const DEFAULT_AGE_PICKER_VALUE = '018'
/**
* 清洗配置里的特殊年龄选项,去掉空值和多余空格。
*/
const normalizeSpecialOptions = (specialOptions = []) => {
return specialOptions
.map(option => String(option ?? '').trim())
.filter(Boolean)
}
/**
* 构建 NutUI Picker 需要的年龄选项列。
* 特殊年龄项会排在最前面,随后才是 0-120 岁的普通年龄。
*/
export function buildAgePickerColumn({ specialOptions = [], minAge = 0, maxAge = 120 } = {}) {
const normalizedSpecialOptions = normalizeSpecialOptions(specialOptions)
const numericOptions = Array.from({ length: maxAge - minAge + 1 }, (_, index) => {
const age = minAge + index
return {
text: `${age} 岁`,
value: String(age).padStart(3, '0')
}
})
const specialAgeOptions = normalizedSpecialOptions.map(option => ({
text: option,
value: `${SPECIAL_AGE_VALUE_PREFIX}${option}`
}))
return [...specialAgeOptions, ...numericOptions]
}
/**
* 将表单里的年龄值转换成输入框展示文案。
* 数字年龄显示为 "X 岁",特殊年龄文本原样显示。
*/
export function formatAgeDisplayValue(value) {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'number' && !Number.isNaN(value)) {
return `${value} 岁`
}
const normalizedValue = String(value).trim()
if (!normalizedValue) {
return ''
}
if (/^\d+$/.test(normalizedValue)) {
return `${parseInt(normalizedValue, 10)} 岁`
}
return normalizedValue
}
/**
* 将外部年龄值转换成 Picker 内部 value。
* 普通年龄会变成三位数字字符串,特殊年龄会追加内部前缀。
*/
export function normalizeAgePickerValue(value, specialOptions = [], defaultValue = DEFAULT_AGE_PICKER_VALUE) {
if (value === null || value === undefined || value === '') {
return defaultValue
}
const normalizedSpecialOptions = normalizeSpecialOptions(specialOptions)
if (typeof value === 'string') {
const normalizedValue = value.trim()
if (normalizedSpecialOptions.includes(normalizedValue)) {
return `${SPECIAL_AGE_VALUE_PREFIX}${normalizedValue}`
}
if (/^\d+$/.test(normalizedValue)) {
return String(parseInt(normalizedValue, 10)).padStart(3, '0')
}
return defaultValue
}
if (typeof value === 'number' && !Number.isNaN(value)) {
return String(value).padStart(3, '0')
}
return defaultValue
}
/**
* 将 Picker 返回值还原成表单实际存储值。
* 特殊年龄返回原始文本,普通年龄返回数字。
*/
export function parseAgePickerValue(value) {
if (typeof value !== 'string') {
return null
}
if (value.startsWith(SPECIAL_AGE_VALUE_PREFIX)) {
return value.slice(SPECIAL_AGE_VALUE_PREFIX.length)
}
const parsedValue = parseInt(value, 10)
return Number.isNaN(parsedValue) ? null : parsedValue
}
......@@ -76,6 +76,20 @@ export function formatAge(value) {
if (value === null || value === undefined) {
return null
}
if (typeof value === 'string') {
const normalizedValue = value.trim()
if (!normalizedValue) {
return null
}
if (!/^\d+$/.test(normalizedValue)) {
return normalizedValue
}
return `${parseInt(normalizedValue, 10)}岁`
}
return `${value}岁`
}
......