hookehuyr

feat(plan): 新增提取期自定义输入功能

- 新增 PeriodInput 组件,支持用户自定义输入提取期(1-100整数)
- SelectPickerGlobal 支持自定义选项入口,允许用户选择"自定义输入"
- SavingsTemplate 集成自定义输入功能,支持多阶段提取期自定义
- 自定义值临时保存到本次会话选项列表,跨阶段复用
- 验证规则:整数年期(1-100年)、快捷选项(终身、一笔过)
- 使用 watch 监听输入值变化,防止小程序 @input 事件丢失

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -36,6 +36,7 @@ declare module 'vue' { ...@@ -36,6 +36,7 @@ declare module 'vue' {
36 OfficeViewer: typeof import('./src/components/documents/OfficeViewer.vue')['default'] 36 OfficeViewer: typeof import('./src/components/documents/OfficeViewer.vue')['default']
37 PaymentPeriodRadio: typeof import('./src/components/plan/PlanFields/PaymentPeriodRadio.vue')['default'] 37 PaymentPeriodRadio: typeof import('./src/components/plan/PlanFields/PaymentPeriodRadio.vue')['default']
38 PdfPreview: typeof import('./src/components/documents/PdfPreview.vue')['default'] 38 PdfPreview: typeof import('./src/components/documents/PdfPreview.vue')['default']
39 + PeriodInput: typeof import('./src/components/plan/PlanFields/PeriodInput.vue')['default']
39 PlanFormContainer: typeof import('./src/components/plan/PlanFormContainer.vue')['default'] 40 PlanFormContainer: typeof import('./src/components/plan/PlanFormContainer.vue')['default']
40 PlanPopupNew: typeof import('./src/components/plan/PlanPopupNew.vue')['default'] 41 PlanPopupNew: typeof import('./src/components/plan/PlanPopupNew.vue')['default']
41 ProductCard: typeof import('./src/components/cards/ProductCard.vue')['default'] 42 ProductCard: typeof import('./src/components/cards/ProductCard.vue')['default']
......
This diff is collapsed. Click to expand it.
...@@ -39,17 +39,30 @@ ...@@ -39,17 +39,30 @@
39 * 39 *
40 * @description 使用 NutUI Picker 实现下拉选择功能 40 * @description 使用 NutUI Picker 实现下拉选择功能
41 * - key 和 value 相同(如"整付(0-75 岁)") 41 * - key 和 value 相同(如"整付(0-75 岁)")
42 - * - 适用于缴费年期等场景 42 + * - 适用于缴费年期、提取期等场景
43 * - 使用 GlobalPopupManager 管理弹窗层级 43 * - 使用 GlobalPopupManager 管理弹窗层级
44 + * - 支持自定义输入选项(可选功能)
44 * @author Claude Code 45 * @author Claude Code
45 - * @version 2.0.0 - 支持全局弹窗管理器 46 + * @version 2.1.0 - 新增自定义输入支持
46 * @example 47 * @example
48 + * // 基础用法
47 * <SelectPickerGlobal 49 * <SelectPickerGlobal
48 * v-model="paymentPeriod" 50 * v-model="paymentPeriod"
49 * label="缴费年期" 51 * label="缴费年期"
50 * placeholder="请选择缴费年期" 52 * placeholder="请选择缴费年期"
51 * :options="['整付(0-75 岁)', '5 年(0-70 岁)']" 53 * :options="['整付(0-75 岁)', '5 年(0-70 岁)']"
52 * /> 54 * />
55 + *
56 + * @example
57 + * // 支持自定义输入
58 + * <SelectPickerGlobal
59 + * v-model="withdrawalPeriod"
60 + * label="提取期"
61 + * placeholder="请选择提取期"
62 + * :options="['1年', '5年', '10年', '终身']"
63 + * :allow-custom="true"
64 + * @custom-select="openCustomInput"
65 + * />
53 */ 66 */
54 import { ref, computed, onMounted } from 'vue' 67 import { ref, computed, onMounted } from 'vue'
55 import IconFont from '@/components/icons/IconFont.vue' 68 import IconFont from '@/components/icons/IconFont.vue'
...@@ -121,6 +134,35 @@ const props = defineProps({ ...@@ -121,6 +134,35 @@ const props = defineProps({
121 options: { 134 options: {
122 type: Array, 135 type: Array,
123 required: true 136 required: true
137 + },
138 +
139 + /**
140 + * 是否允许自定义输入
141 + * @type {boolean}
142 + * @description 开启后,选项列表末尾会添加"自定义输入"选项
143 + */
144 + allowCustom: {
145 + type: Boolean,
146 + default: false
147 + },
148 +
149 + /**
150 + * 自定义选项的显示文本
151 + * @type {string}
152 + */
153 + customLabel: {
154 + type: String,
155 + default: '📝 自定义输入...'
156 + },
157 +
158 + /**
159 + * 分隔符文本
160 + * @type {string}
161 + * @description 自定义选项与标准选项之间的分隔线
162 + */
163 + dividerLabel: {
164 + type: String,
165 + default: '──────────'
124 } 166 }
125 }) 167 })
126 168
...@@ -143,7 +185,12 @@ const emit = defineEmits([ ...@@ -143,7 +185,12 @@ const emit = defineEmits([
143 * 弹窗关闭事件 185 * 弹窗关闭事件
144 * @event close 186 * @event close
145 */ 187 */
146 - 'close' 188 + 'close',
189 + /**
190 + * 用户选择自定义输入选项
191 + * @event custom-select
192 + */
193 + 'custom-select'
147 ]) 194 ])
148 195
149 /** 196 /**
...@@ -167,15 +214,34 @@ const openPicker = () => { ...@@ -167,15 +214,34 @@ const openPicker = () => {
167 /** 214 /**
168 * 转换为 Picker 格式 215 * 转换为 Picker 格式
169 * @description 将选项数组转换为 Picker 需要的格式 216 * @description 将选项数组转换为 Picker 需要的格式
217 + * 如果允许自定义,会在末尾添加分隔符和自定义选项
170 * @example 218 * @example
171 * // options = ['整付(0-75 岁)', '5 年(0-70 岁)'] 219 * // options = ['整付(0-75 岁)', '5 年(0-70 岁)']
172 * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...] 220 * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...]
173 */ 221 */
174 const pickerColumns = computed(() => { 222 const pickerColumns = computed(() => {
175 - return props.options.map(option => ({ 223 + const standardOptions = props.options.map(option => ({
176 text: option, 224 text: option,
177 value: option // key 和 value 相同 225 value: option // key 和 value 相同
178 })) 226 }))
227 +
228 + // 如果允许自定义,添加分隔符和自定义选项
229 + if (props.allowCustom) {
230 + return [
231 + ...standardOptions,
232 + {
233 + text: props.dividerLabel,
234 + value: '__divider__',
235 + disabled: true // 分隔符不可选
236 + },
237 + {
238 + text: props.customLabel,
239 + value: '__custom__'
240 + }
241 + ]
242 + }
243 +
244 + return standardOptions
179 }) 245 })
180 246
181 /** 247 /**
...@@ -197,6 +263,27 @@ const displayValue = computed(() => { ...@@ -197,6 +263,27 @@ const displayValue = computed(() => {
197 */ 263 */
198 const onConfirm = ({ selectedOptions }) => { 264 const onConfirm = ({ selectedOptions }) => {
199 const value = selectedOptions[0]?.value 265 const value = selectedOptions[0]?.value
266 +
267 + // 处理自定义选项
268 + if (value === '__custom__') {
269 + // 触发自定义选择事件,由父组件处理
270 + emit('custom-select')
271 +
272 + // 停用弹窗
273 + if (popupId.value) {
274 + deactivatePopup(popupId.value)
275 + }
276 + showPicker.value = false
277 + emit('close')
278 + return
279 + }
280 +
281 + // 跳过分隔符
282 + if (value === '__divider__') {
283 + return
284 + }
285 +
286 + // 标准选项处理
200 if (value !== undefined) { 287 if (value !== undefined) {
201 emit('update:modelValue', value) 288 emit('update:modelValue', value)
202 } 289 }
......
...@@ -92,7 +92,9 @@ ...@@ -92,7 +92,9 @@
92 label="提取期" 92 label="提取期"
93 placeholder="请选择提取期" 93 placeholder="请选择提取期"
94 :required="true" 94 :required="true"
95 - :options="multiStagePeriodOptions" 95 + :options="dynamicPeriodOptions"
96 + :allow-custom="isCustomPeriodEnabled"
97 + @custom-select="openPeriodInput(index)"
96 class="mb-3" 98 class="mb-3"
97 /> 99 />
98 100
...@@ -142,6 +144,17 @@ ...@@ -142,6 +144,17 @@
142 <p>⚠️ 模板配置未找到</p> 144 <p>⚠️ 模板配置未找到</p>
143 <p class="text-sm mt-2">请检查产品配置或联系开发人员</p> 145 <p class="text-sm mt-2">请检查产品配置或联系开发人员</p>
144 </div> 146 </div>
147 +
148 + <!-- 自定义提取期输入弹窗 -->
149 + <PeriodInput
150 + v-model:visible="showPeriodInput"
151 + v-model="currentPeriodValue"
152 + inputLabel="请输入提取期"
153 + inputPlaceholder="请输入年数"
154 + :validation-rules="periodValidationRules"
155 + @confirm="onPeriodInputConfirm"
156 + @cancel="onPeriodInputCancel"
157 + />
145 </template> 158 </template>
146 159
147 <script setup> 160 <script setup>
...@@ -167,6 +180,7 @@ import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' ...@@ -167,6 +180,7 @@ import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
167 import PlanFieldRadio from '../PlanFields/RadioGroup.vue' 180 import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
168 import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' 181 import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
169 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue' 182 import PaymentPeriodRadio from '../PlanFields/PaymentPeriodRadio.vue'
183 +import PeriodInput from '../PlanFields/PeriodInput.vue'
170 import { useFieldDependencies } from '@/composables/useFieldDependencies' 184 import { useFieldDependencies } from '@/composables/useFieldDependencies'
171 185
172 /** 186 /**
...@@ -387,6 +401,45 @@ const canRemoveStage = computed(() => { ...@@ -387,6 +401,45 @@ const canRemoveStage = computed(() => {
387 }) 401 })
388 402
389 /** 403 /**
404 + * 自定义提取期输入状态
405 + */
406 +const showPeriodInput = ref(false) // 自定义输入弹窗显示状态
407 +const currentPeriodValue = ref('') // 当前输入的提取期值
408 +const currentStageIndex = ref(-1) // 当前正在编辑的阶段索引
409 +const customPeriodValues = ref([]) // 用户自定义的提取期值列表(临时保存)
410 +
411 +/**
412 + * 自定义提取期是否启用
413 + */
414 +const isCustomPeriodEnabled = computed(() => {
415 + return multiStageConfig.value.custom_period?.enabled || false
416 +})
417 +
418 +/**
419 + * 自定义提取期验证规则
420 + */
421 +const periodValidationRules = computed(() => {
422 + const config = multiStageConfig.value.custom_period?.validation || {}
423 + return {
424 + min: config.min_years ?? 1,
425 + max: config.max_years ?? 100,
426 + allowed_formats: config.allowed_formats || ['终身', '一笔过'],
427 + custom_validators: config.custom_validators || []
428 + }
429 +})
430 +
431 +/**
432 + * 动态提取期选项(预设选项 + 用户自定义选项)
433 + */
434 +const dynamicPeriodOptions = computed(() => {
435 + const baseOptions = multiStageConfig.value.withdrawal_periods ||
436 + props.config?.withdrawal_plan?.withdrawal_periods ||
437 + []
438 + // 合并预设选项和用户自定义选项
439 + return [...baseOptions, ...customPeriodValues.value]
440 +})
441 +
442 +/**
390 * 创建空的阶段数据 443 * 创建空的阶段数据
391 * @returns {Object} 空阶段对象 444 * @returns {Object} 空阶段对象
392 */ 445 */
...@@ -445,6 +498,50 @@ const removeStage = (index) => { ...@@ -445,6 +498,50 @@ const removeStage = (index) => {
445 } 498 }
446 499
447 /** 500 /**
501 + * 打开自定义提取期输入弹窗
502 + * @param {number} stageIndex - 阶段索引
503 + */
504 +const openPeriodInput = (stageIndex) => {
505 + currentStageIndex.value = stageIndex
506 + const stage = stages.value[stageIndex]
507 + currentPeriodValue.value = stage?.withdrawal_period || ''
508 + showPeriodInput.value = true
509 +}
510 +
511 +/**
512 + * 自定义提取期输入确认
513 + * @param {string} value - 确认的提取期值
514 + */
515 +const onPeriodInputConfirm = (value) => {
516 + // 添加到自定义值列表(去重:不在预设选项和已存在的自定义列表中)
517 + const baseOptions = multiStageConfig.value.withdrawal_periods ||
518 + props.config?.withdrawal_plan?.withdrawal_periods ||
519 + []
520 + if (!baseOptions.includes(value) && !customPeriodValues.value.includes(value)) {
521 + customPeriodValues.value.push(value)
522 + }
523 +
524 + // 更新当前阶段的值
525 + if (currentStageIndex.value >= 0 && currentStageIndex.value < stages.value.length) {
526 + stages.value[currentStageIndex.value].withdrawal_period = value
527 + }
528 +
529 + // 关闭弹窗并重置状态
530 + showPeriodInput.value = false
531 + currentStageIndex.value = -1
532 + currentPeriodValue.value = ''
533 +}
534 +
535 +/**
536 + * 自定义提取期输入取消
537 + */
538 +const onPeriodInputCancel = () => {
539 + showPeriodInput.value = false
540 + currentStageIndex.value = -1
541 + currentPeriodValue.value = ''
542 +}
543 +
544 +/**
448 * 同步阶段数据到表单 545 * 同步阶段数据到表单
449 * @description 将 stages 数组同步到 form.withdrawal_stages,以便父组件获取 546 * @description 将 stages 数组同步到 form.withdrawal_stages,以便父组件获取
450 * 同时清理 undefined 值为 null,确保提交数据格式正确 547 * 同时清理 undefined 值为 null,确保提交数据格式正确
......
...@@ -123,6 +123,7 @@ const savingsFormSchema = { ...@@ -123,6 +123,7 @@ const savingsFormSchema = {
123 * 多阶段提取计划配置 123 * 多阶段提取计划配置
124 * @description 用于"宏挚传承保障计划(多阶段)"等支持多阶段提取的产品 124 * @description 用于"宏挚传承保障计划(多阶段)"等支持多阶段提取的产品
125 * @updated 2026-02-25 - 新增多阶段提取功能 125 * @updated 2026-02-25 - 新增多阶段提取功能
126 + * @updated 2026-02-28 - 新增自定义提取期输入功能
126 */ 127 */
127 const multiStageWithdrawalConfig = { 128 const multiStageWithdrawalConfig = {
128 enabled: true, 129 enabled: true,
...@@ -133,7 +134,20 @@ const multiStageWithdrawalConfig = { ...@@ -133,7 +134,20 @@ const multiStageWithdrawalConfig = {
133 '10年', '15年', '20年', '终身', 134 '10年', '15年', '20年', '终身',
134 '一笔过' // 新增:一次性提取选项 135 '一笔过' // 新增:一次性提取选项
135 ], 136 ],
136 - percentage_optional: true // 递增百分比可选 137 + percentage_optional: true, // 递增百分比可选
138 +
139 + // 自定义提取期配置(新增)
140 + custom_period: {
141 + enabled: true, // 是否允许自定义输入
142 + custom_label: '📝 自定义输入...', // 自定义选项显示文本
143 + validation: {
144 + min_years: 1, // 最小年期(整数)
145 + max_years: 100, // 最大年期(整数)
146 + allowed_formats: ['终身', '一笔过'], // 允许的非年期格式
147 + // 预留:未来可扩展更多格式
148 + custom_validators: [] // 自定义验证函数数组
149 + }
150 + }
137 } 151 }
138 152
139 export const PLAN_TEMPLATES = { 153 export const PLAN_TEMPLATES = {
......