hookehuyr

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

...@@ -13,7 +13,7 @@ const Api = { ...@@ -13,7 +13,7 @@ const Api = {
13 * @param {Object} params 请求参数 13 * @param {Object} params 请求参数
14 * @param {string} params.customer_name 申请人 14 * @param {string} params.customer_name 申请人
15 * @param {string} params.customer_gender 性别 15 * @param {string} params.customer_gender 性别
16 - * @param {integer} params.customer_age 年龄 16 + * @param {string} params.customer_age 年龄
17 * @param {string} params.customer_birthday 出生年月日 17 * @param {string} params.customer_birthday 出生年月日
18 * @param {integer} params.annual_premium 年缴保费 18 * @param {integer} params.annual_premium 年缴保费
19 * @param {string} params.payment_years 繳費年期 19 * @param {string} params.payment_years 繳費年期
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
43 * - 显示格式:3位数字(如 018 表示 18 岁) 43 * - 显示格式:3位数字(如 018 表示 18 岁)
44 * - 提交格式:数字(如 18) 44 * - 提交格式:数字(如 18)
45 * - 年龄范围:0-120 岁 45 * - 年龄范围:0-120 岁
46 + * - 支持按产品配置注入特殊年龄选项(如“孕22周”)
46 * - 使用 GlobalPopupManager 管理弹窗层级 47 * - 使用 GlobalPopupManager 管理弹窗层级
47 * @author Claude Code 48 * @author Claude Code
48 * @version 2.0.0 - 支持全局弹窗管理器 49 * @version 2.0.0 - 支持全局弹窗管理器
...@@ -56,6 +57,13 @@ ...@@ -56,6 +57,13 @@
56 import { ref, computed, watch, onMounted } from 'vue' 57 import { ref, computed, watch, onMounted } from 'vue'
57 import IconFont from '@/components/icons/IconFont.vue' 58 import IconFont from '@/components/icons/IconFont.vue'
58 import { useGlobalPopup } from './GlobalPopupManager' 59 import { useGlobalPopup } from './GlobalPopupManager'
60 +import {
61 + buildAgePickerColumn,
62 + DEFAULT_AGE_PICKER_VALUE,
63 + formatAgeDisplayValue,
64 + normalizeAgePickerValue,
65 + parseAgePickerValue
66 +} from '@/utils/agePickerOptions'
59 67
60 /** 68 /**
61 * 使用全局弹窗管理器 69 * 使用全局弹窗管理器
...@@ -107,12 +115,21 @@ const props = defineProps({ ...@@ -107,12 +115,21 @@ const props = defineProps({
107 }, 115 },
108 116
109 /** 117 /**
110 - * 绑定的值(数字) 118 + * 绑定的值(数字年龄或特殊年龄文本
111 - * @type {number} 119 + * @type {number|string}
112 */ 120 */
113 modelValue: { 121 modelValue: {
114 - type: Number, 122 + type: [Number, String],
115 default: null 123 default: null
124 + },
125 +
126 + /**
127 + * 产品级特殊年龄选项
128 + * @type {Array<string>}
129 + */
130 + specialOptions: {
131 + type: Array,
132 + default: () => []
116 } 133 }
117 }) 134 })
118 135
...@@ -143,17 +160,14 @@ const showPicker = ref(false) ...@@ -143,17 +160,14 @@ const showPicker = ref(false)
143 /** 160 /**
144 * Picker 当前值(3位数字格式) 161 * Picker 当前值(3位数字格式)
145 */ 162 */
146 -const pickerValue = ref(['018']) 163 +const pickerValue = ref([DEFAULT_AGE_PICKER_VALUE])
147 164
148 /** 165 /**
149 * 年龄选项列(0-120 岁,3位数字格式) 166 * 年龄选项列(0-120 岁,3位数字格式)
150 */ 167 */
151 const ageColumns = computed(() => { 168 const ageColumns = computed(() => {
152 return [ 169 return [
153 - Array.from({ length: 121 }, (_, i) => ({ 170 + buildAgePickerColumn({ specialOptions: props.specialOptions })
154 - text: `${i} 岁`,
155 - value: String(i).padStart(3, '0')
156 - }))
157 ] 171 ]
158 }) 172 })
159 173
...@@ -161,10 +175,7 @@ const ageColumns = computed(() => { ...@@ -161,10 +175,7 @@ const ageColumns = computed(() => {
161 * 显示的值(转换为中文格式) 175 * 显示的值(转换为中文格式)
162 */ 176 */
163 const displayValue = computed(() => { 177 const displayValue = computed(() => {
164 - if (props.modelValue === null || props.modelValue === undefined) { 178 + return formatAgeDisplayValue(props.modelValue)
165 - return ''
166 - }
167 - return `${props.modelValue} 岁`
168 }) 179 })
169 180
170 /** 181 /**
...@@ -178,7 +189,7 @@ const handleTap = () => { ...@@ -178,7 +189,7 @@ const handleTap = () => {
178 189
179 // 如果有值,转换为3位数字格式 190 // 如果有值,转换为3位数字格式
180 if (props.modelValue !== null && props.modelValue !== undefined) { 191 if (props.modelValue !== null && props.modelValue !== undefined) {
181 - pickerValue.value = [String(props.modelValue).padStart(3, '0')] 192 + pickerValue.value = [normalizeAgePickerValue(props.modelValue, props.specialOptions)]
182 } 193 }
183 194
184 showPicker.value = true 195 showPicker.value = true
...@@ -193,8 +204,7 @@ const handleTap = () => { ...@@ -193,8 +204,7 @@ const handleTap = () => {
193 * onConfirm({ selectedValue: ['018'] }) 204 * onConfirm({ selectedValue: ['018'] })
194 */ 205 */
195 const onConfirm = ({ selectedValue }) => { 206 const onConfirm = ({ selectedValue }) => {
196 - // 将3位数字格式转换为普通数字 207 + const age = parseAgePickerValue(selectedValue[0])
197 - const age = parseInt(selectedValue[0], 10)
198 208
199 emit('update:modelValue', age) 209 emit('update:modelValue', age)
200 emit('change', age) 210 emit('change', age)
...@@ -226,7 +236,7 @@ watch( ...@@ -226,7 +236,7 @@ watch(
226 () => props.modelValue, 236 () => props.modelValue,
227 (newVal) => { 237 (newVal) => {
228 if (newVal !== null && newVal !== undefined) { 238 if (newVal !== null && newVal !== undefined) {
229 - pickerValue.value = [String(newVal).padStart(3, '0')] 239 + pickerValue.value = [normalizeAgePickerValue(newVal, props.specialOptions)]
230 } 240 }
231 } 241 }
232 ) 242 )
......
...@@ -72,6 +72,7 @@ const props = defineProps({ ...@@ -72,6 +72,7 @@ const props = defineProps({
72 * @type {Object} 72 * @type {Object}
73 * @property {string} currency - 币种代码 73 * @property {string} currency - 币种代码
74 * @property {Array<string>} payment_periods - 缴费年期选项 74 * @property {Array<string>} payment_periods - 缴费年期选项
75 + * @property {Array<string>} special_age_options - 产品专属年龄特殊选项
75 * @property {Object} age_range - 年龄范围 { min, max } 76 * @property {Object} age_range - 年龄范围 { min, max }
76 * @property {string} insurance_period - 保险期间 77 * @property {string} insurance_period - 保险期间
77 * @property {Object} form_schema - 表单 Schema 78 * @property {Object} form_schema - 表单 Schema
...@@ -152,6 +153,10 @@ const getFieldProps = (field) => { ...@@ -152,6 +153,10 @@ const getFieldProps = (field) => {
152 fieldProps.options = field.options 153 fieldProps.options = field.options
153 } 154 }
154 155
156 + if (field.key === 'age' && Array.isArray(props.config?.special_age_options)) {
157 + fieldProps.specialOptions = props.config.special_age_options
158 + }
159 +
155 // 缴费年期选项由模板配置提供 160 // 缴费年期选项由模板配置提供
156 if (field.options_from === 'payment_periods') { 161 if (field.options_from === 'payment_periods') {
157 fieldProps.options = fieldProps.options || props.config?.payment_periods 162 fieldProps.options = fieldProps.options || props.config?.payment_periods
......
...@@ -44,3 +44,11 @@ describe('plan field definitions amount semantics', () => { ...@@ -44,3 +44,11 @@ describe('plan field definitions amount semantics', () => {
44 }) 44 })
45 }) 45 })
46 }) 46 })
47 +
48 +describe('critical illness mpc special age options', () => {
49 + it('should expose pregnancy week option only for mpc', () => {
50 + expect(PLAN_TEMPLATES['critical-illness-mpc'].config.special_age_options).toEqual(['孕22周'])
51 + expect(PLAN_TEMPLATES['critical-illness-mbc-pro'].config.special_age_options).toBeUndefined()
52 + expect(PLAN_TEMPLATES['critical-illness-mbc2'].config.special_age_options).toBeUndefined()
53 + })
54 +})
......
...@@ -197,6 +197,7 @@ export const PLAN_TEMPLATES = { ...@@ -197,6 +197,7 @@ export const PLAN_TEMPLATES = {
197 component: 'CriticalIllnessTemplate', 197 component: 'CriticalIllnessTemplate',
198 config: { 198 config: {
199 currency: 'USD', 199 currency: 'USD',
200 + special_age_options: ['孕22周'], // 仅 MPC 使用:年龄字段增加前置特殊选项
200 payment_periods: [ 201 payment_periods: [
201 '10 年(15 日 - 65 岁)', 202 '10 年(15 日 - 65 岁)',
202 '20 年(15 日 - 65 岁)', 203 '20 年(15 日 - 65 岁)',
......
1 +import { describe, expect, it } from 'vitest'
2 +import {
3 + buildAgePickerColumn,
4 + formatAgeDisplayValue,
5 + normalizeAgePickerValue,
6 + parseAgePickerValue,
7 + SPECIAL_AGE_VALUE_PREFIX
8 +} from '../agePickerOptions'
9 +
10 +describe('agePickerOptions', () => {
11 + it('should prepend special age options before numeric ages', () => {
12 + const column = buildAgePickerColumn({ specialOptions: ['孕22周'] })
13 +
14 + expect(column[0]).toEqual({
15 + text: '孕22周',
16 + value: `${SPECIAL_AGE_VALUE_PREFIX}孕22周`
17 + })
18 + expect(column[1]).toEqual({
19 + text: '0 岁',
20 + value: '000'
21 + })
22 + })
23 +
24 + it('should format special and numeric age display values', () => {
25 + expect(formatAgeDisplayValue('孕22周')).toBe('孕22周')
26 + expect(formatAgeDisplayValue(0)).toBe('0 岁')
27 + expect(formatAgeDisplayValue('12')).toBe('12 岁')
28 + })
29 +
30 + it('should normalize and parse picker values correctly', () => {
31 + expect(normalizeAgePickerValue('孕22周', ['孕22周'])).toBe(`${SPECIAL_AGE_VALUE_PREFIX}孕22周`)
32 + expect(normalizeAgePickerValue(3, ['孕22周'])).toBe('003')
33 + expect(parseAgePickerValue(`${SPECIAL_AGE_VALUE_PREFIX}孕22周`)).toBe('孕22周')
34 + expect(parseAgePickerValue('003')).toBe(3)
35 + })
36 +})
...@@ -72,6 +72,10 @@ describe('formatAge', () => { ...@@ -72,6 +72,10 @@ describe('formatAge', () => {
72 expect(formatAge(0)).toBe('0岁') 72 expect(formatAge(0)).toBe('0岁')
73 }) 73 })
74 74
75 + it('should keep special age labels unchanged', () => {
76 + expect(formatAge('孕22周')).toBe('孕22周')
77 + })
78 +
75 it('should handle null and undefined', () => { 79 it('should handle null and undefined', () => {
76 expect(formatAge(null)).toBe(null) 80 expect(formatAge(null)).toBe(null)
77 expect(formatAge(undefined)).toBe(null) 81 expect(formatAge(undefined)).toBe(null)
......
1 +/**
2 + * 特殊年龄选项在 Picker 内部使用字符串前缀编码,
3 + * 用来和普通数字年龄值(如 "003")区分。
4 + */
5 +export const SPECIAL_AGE_VALUE_PREFIX = '__special_age__:'
6 +
7 +/**
8 + * 未选择年龄时,Picker 默认定位到 18 岁。
9 + */
10 +export const DEFAULT_AGE_PICKER_VALUE = '018'
11 +
12 +/**
13 + * 清洗配置里的特殊年龄选项,去掉空值和多余空格。
14 + */
15 +const normalizeSpecialOptions = (specialOptions = []) => {
16 + return specialOptions
17 + .map(option => String(option ?? '').trim())
18 + .filter(Boolean)
19 +}
20 +
21 +/**
22 + * 构建 NutUI Picker 需要的年龄选项列。
23 + * 特殊年龄项会排在最前面,随后才是 0-120 岁的普通年龄。
24 + */
25 +export function buildAgePickerColumn({ specialOptions = [], minAge = 0, maxAge = 120 } = {}) {
26 + const normalizedSpecialOptions = normalizeSpecialOptions(specialOptions)
27 + const numericOptions = Array.from({ length: maxAge - minAge + 1 }, (_, index) => {
28 + const age = minAge + index
29 + return {
30 + text: `${age} 岁`,
31 + value: String(age).padStart(3, '0')
32 + }
33 + })
34 +
35 + const specialAgeOptions = normalizedSpecialOptions.map(option => ({
36 + text: option,
37 + value: `${SPECIAL_AGE_VALUE_PREFIX}${option}`
38 + }))
39 +
40 + return [...specialAgeOptions, ...numericOptions]
41 +}
42 +
43 +/**
44 + * 将表单里的年龄值转换成输入框展示文案。
45 + * 数字年龄显示为 "X 岁",特殊年龄文本原样显示。
46 + */
47 +export function formatAgeDisplayValue(value) {
48 + if (value === null || value === undefined) {
49 + return ''
50 + }
51 +
52 + if (typeof value === 'number' && !Number.isNaN(value)) {
53 + return `${value} 岁`
54 + }
55 +
56 + const normalizedValue = String(value).trim()
57 + if (!normalizedValue) {
58 + return ''
59 + }
60 +
61 + if (/^\d+$/.test(normalizedValue)) {
62 + return `${parseInt(normalizedValue, 10)} 岁`
63 + }
64 +
65 + return normalizedValue
66 +}
67 +
68 +/**
69 + * 将外部年龄值转换成 Picker 内部 value。
70 + * 普通年龄会变成三位数字字符串,特殊年龄会追加内部前缀。
71 + */
72 +export function normalizeAgePickerValue(value, specialOptions = [], defaultValue = DEFAULT_AGE_PICKER_VALUE) {
73 + if (value === null || value === undefined || value === '') {
74 + return defaultValue
75 + }
76 +
77 + const normalizedSpecialOptions = normalizeSpecialOptions(specialOptions)
78 +
79 + if (typeof value === 'string') {
80 + const normalizedValue = value.trim()
81 + if (normalizedSpecialOptions.includes(normalizedValue)) {
82 + return `${SPECIAL_AGE_VALUE_PREFIX}${normalizedValue}`
83 + }
84 +
85 + if (/^\d+$/.test(normalizedValue)) {
86 + return String(parseInt(normalizedValue, 10)).padStart(3, '0')
87 + }
88 +
89 + return defaultValue
90 + }
91 +
92 + if (typeof value === 'number' && !Number.isNaN(value)) {
93 + return String(value).padStart(3, '0')
94 + }
95 +
96 + return defaultValue
97 +}
98 +
99 +/**
100 + * 将 Picker 返回值还原成表单实际存储值。
101 + * 特殊年龄返回原始文本,普通年龄返回数字。
102 + */
103 +export function parseAgePickerValue(value) {
104 + if (typeof value !== 'string') {
105 + return null
106 + }
107 +
108 + if (value.startsWith(SPECIAL_AGE_VALUE_PREFIX)) {
109 + return value.slice(SPECIAL_AGE_VALUE_PREFIX.length)
110 + }
111 +
112 + const parsedValue = parseInt(value, 10)
113 + return Number.isNaN(parsedValue) ? null : parsedValue
114 +}
...@@ -76,6 +76,20 @@ export function formatAge(value) { ...@@ -76,6 +76,20 @@ export function formatAge(value) {
76 if (value === null || value === undefined) { 76 if (value === null || value === undefined) {
77 return null 77 return null
78 } 78 }
79 +
80 + if (typeof value === 'string') {
81 + const normalizedValue = value.trim()
82 + if (!normalizedValue) {
83 + return null
84 + }
85 +
86 + if (!/^\d+$/.test(normalizedValue)) {
87 + return normalizedValue
88 + }
89 +
90 + return `${parseInt(normalizedValue, 10)}岁`
91 + }
92 +
79 return `${value}岁` 93 return `${value}岁`
80 } 94 }
81 95
......