Showing
9 changed files
with
209 additions
and
17 deletions
| ... | @@ -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 岁)', | ... | ... |
src/utils/__tests__/agePickerOptions.test.js
0 → 100644
| 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) | ... | ... |
src/utils/agePickerOptions.js
0 → 100644
| 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 | ... | ... |
-
Please register or login to post a comment