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']
......
1 +<template>
2 + <view>
3 + <!-- 标签 -->
4 + <view v-if="label" class="text-sm text-gray-600 mb-2 flex items-center">
5 + <text v-if="required" class="text-red-500 mr-1">*</text>
6 + <text>{{ label }}</text>
7 + </view>
8 +
9 + <!-- 触发区域 -->
10 + <view
11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 + @tap="openInput"
13 + >
14 + <text :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
15 + {{ displayValue || placeholder }}
16 + </text>
17 + <IconFont name="right" size="14" color="#9CA3AF" />
18 + </view>
19 +
20 + <!-- 自定义输入弹窗 -->
21 + <nut-popup
22 + v-model:visible="showInputModal"
23 + position="bottom"
24 + :overlay="true"
25 + :close-on-click-overlay="false"
26 + :style="{ padding: '0', borderRadius: '24rpx', overflow: 'hidden' }"
27 + :overlay-style="{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }"
28 + >
29 + <view class="flex flex-col bg-white">
30 + <!-- 标题栏 -->
31 + <view class="flex justify-between items-center p-4 border-b border-gray-100">
32 + <text class="text-base font-semibold text-gray-900">{{ inputLabel || '请输入提取期' }}</text>
33 + <view class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100" @tap="onCancel">
34 + <text class="text-xl text-gray-500 leading-none">×</text>
35 + </view>
36 + </view>
37 +
38 + <!-- 输入区域 -->
39 + <view class="p-4">
40 + <!-- 数字输入 + 单位 -->
41 + <view class="flex items-center border border-gray-300 rounded-lg px-4 py-3 mb-3 bg-gray-50">
42 + <input
43 + v-model="inputValue"
44 + class="flex-1 text-base text-gray-900 bg-transparent border-none outline-none"
45 + type="digit"
46 + :placeholder="inputPlaceholder"
47 + @input="onInputChange"
48 + />
49 + <text class="text-base text-gray-500 ml-2"></text>
50 + </view>
51 +
52 + <!-- 验证提示 -->
53 + <view class="mb-4 min-h-[20rpx]" :class="isValid && inputValue ? 'text-green-500' : 'text-gray-500'">
54 + <text v-if="!inputValue" class="text-xs">{{ validationHint }}</text>
55 + <text v-else-if="isValid" class="text-xs"> 格式正确</text>
56 + <text v-else class="text-xs text-red-500"> {{ validationError }}</text>
57 + </view>
58 +
59 + <!-- 快捷选项 -->
60 + <view class="flex flex-wrap items-center gap-2">
61 + <text class="text-sm text-gray-500">快捷选项:</text>
62 + <view
63 + v-for="option in quickOptions"
64 + :key="option"
65 + class="px-4 py-2 rounded-lg border text-sm transition-colors"
66 + :class="inputValue === option ? 'bg-blue-50 border-blue-500 text-blue-600' : 'bg-white border-gray-300 text-gray-700'"
67 + @tap="selectQuickOption(option)"
68 + >
69 + <text>{{ option }}</text>
70 + </view>
71 + </view>
72 + </view>
73 +
74 + <!-- 操作按钮 -->
75 + <view class="flex gap-3 p-4 border-t border-gray-100">
76 + <view
77 + class="flex-1 h-11 flex items-center justify-center rounded-lg text-base bg-gray-100 text-gray-700"
78 + @tap="onCancel"
79 + >
80 + <text>取消</text>
81 + </view>
82 + <view
83 + class="flex-1 h-11 flex items-center justify-center rounded-lg text-base transition-colors"
84 + :class="canConfirm ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-400'"
85 + @tap="onConfirm"
86 + >
87 + <text>确定</text>
88 + </view>
89 + </view>
90 + </view>
91 + </nut-popup>
92 + </view>
93 +</template>
94 +
95 +<script setup>
96 +/**
97 + * 提取期自定义输入组件
98 + *
99 + * @description 用于输入自定义的提取期,支持整数年期(1-100年)和快捷选项
100 + * - 整数约束:只接受整数年期(1-100)
101 + * - 快捷选项:终身、一笔过
102 + * - 实时验证:输入时即时反馈格式是否正确
103 + * - 可扩展:预留 customValidators 接口支持未来格式扩展
104 + *
105 + * @module components/plan/PlanFields/PeriodInput
106 + * @author Claude Code
107 + * @version 1.0.0
108 + *
109 + * @example
110 + * <PeriodInput
111 + * v-model:visible="showPeriodInput"
112 + * v-model="periodValue"
113 + * label="提取期"
114 + * inputLabel="请输入提取期"
115 + * :validation-rules="{ min: 1, max: 100 }"
116 + * @confirm="handlePeriodConfirm"
117 + * />
118 + */
119 +import { ref, computed, watch } from 'vue'
120 +import IconFont from '@/components/icons/IconFont.vue'
121 +
122 +/**
123 + * 组件属性
124 + */
125 +const props = defineProps({
126 + /**
127 + * 弹窗显示状态(v-model:visible)
128 + * @type {boolean}
129 + */
130 + visible: {
131 + type: Boolean,
132 + default: false
133 + },
134 +
135 + /**
136 + * 绑定的值(v-model)
137 + * @type {string}
138 + */
139 + modelValue: {
140 + type: String,
141 + default: ''
142 + },
143 +
144 + /**
145 + * 标签文本
146 + * @type {string}
147 + */
148 + label: {
149 + type: String,
150 + default: ''
151 + },
152 +
153 + /**
154 + * 是否必填
155 + * @type {boolean}
156 + */
157 + required: {
158 + type: Boolean,
159 + default: false
160 + },
161 +
162 + /**
163 + * 占位符文本
164 + * @type {string}
165 + */
166 + placeholder: {
167 + type: String,
168 + default: '请选择或输入提取期'
169 + },
170 +
171 + /**
172 + * 弹窗内输入提示文本
173 + * @type {string}
174 + */
175 + inputLabel: {
176 + type: String,
177 + default: '请输入提取期'
178 + },
179 +
180 + /**
181 + * 输入框占位符
182 + * @type {string}
183 + */
184 + inputPlaceholder: {
185 + type: String,
186 + default: '请输入年数'
187 + },
188 +
189 + /**
190 + * 验证规则
191 + * @type {Object}
192 + * @property {number} min - 最小年期(默认1)
193 + * @property {number} max - 最大年期(默认100)
194 + * @property {string[]} allowed_formats - 允许的非年期格式(如['终身', '一笔过'])
195 + * @property {Function[]} custom_validators - 自定义验证函数数组(预留扩展)
196 + */
197 + validationRules: {
198 + type: Object,
199 + default: () => ({
200 + min: 1,
201 + max: 100,
202 + allowed_formats: ['终身', '一笔过'],
203 + custom_validators: []
204 + })
205 + }
206 +})
207 +
208 +/**
209 + * 组件事件
210 + */
211 +const emit = defineEmits([
212 + /**
213 + * 更新弹窗显示状态(v-model:visible)
214 + * @event update:visible
215 + * @param {boolean} value - 弹窗显示状态
216 + */
217 + 'update:visible',
218 +
219 + /**
220 + * 更新绑定值(v-model)
221 + * @event update:modelValue
222 + * @param {string} value - 选中的提取期值
223 + */
224 + 'update:modelValue',
225 +
226 + /**
227 + * 确认输入事件
228 + * @event confirm
229 + * @param {string} value - 确认的提取期值
230 + */
231 + 'confirm',
232 +
233 + /**
234 + * 取消输入事件
235 + * @event cancel
236 + */
237 + 'cancel'
238 +])
239 +
240 +/**
241 + * 弹窗显示状态
242 + */
243 +const showInputModal = ref(false)
244 +
245 +/**
246 + * 输入框的值(原始输入)
247 + */
248 +const inputValue = ref('')
249 +
250 +/**
251 + * 当前验证状态
252 + */
253 +const isValid = ref(false)
254 +
255 +/**
256 + * 验证错误信息
257 + */
258 +const validationError = ref('')
259 +
260 +/**
261 + * 验证提示信息(无输入时显示)
262 + */
263 +const validationHint = ref('')
264 +
265 +/**
266 + * 快捷选项列表
267 + */
268 +const quickOptions = computed(() => {
269 + return props.validationRules?.allowed_formats || ['终身', '一笔过']
270 +})
271 +
272 +/**
273 + * 显示的值
274 + */
275 +const displayValue = computed(() => {
276 + return props.modelValue || ''
277 +})
278 +
279 +/**
280 + * 是否可以确认
281 + */
282 +const canConfirm = computed(() => {
283 + return isValid.value && inputValue.value
284 +})
285 +
286 +/**
287 + * 监听 visible prop 变化
288 + */
289 +watch(() => props.visible, (newVal) => {
290 + showInputModal.value = newVal
291 + if (newVal) {
292 + // 打开弹窗时,初始化输入值
293 + initInputValue()
294 + }
295 +})
296 +
297 +/**
298 + * 监听弹窗状态变化,同步到父组件
299 + */
300 +watch(showInputModal, (newVal) => {
301 + if (!newVal) {
302 + emit('update:visible', false)
303 + }
304 +})
305 +
306 +/**
307 + * 监听输入值变化,触发验证
308 + */
309 +watch(inputValue, () => {
310 + // 每次输入值变化都触发验证(防止 @input 事件丢失)
311 + validateInput()
312 +})
313 +
314 +/**
315 + * 初始化输入值
316 + */
317 +const initInputValue = () => {
318 + if (props.modelValue) {
319 + // 如果是快捷选项,直接使用
320 + if (quickOptions.value.includes(props.modelValue)) {
321 + inputValue.value = props.modelValue
322 + } else {
323 + // 如果是年期格式,提取数字
324 + const match = props.modelValue.match(/^(\d+)年$/)
325 + if (match) {
326 + inputValue.value = match[1] // 只存储数字
327 + } else {
328 + inputValue.value = props.modelValue
329 + }
330 + }
331 + } else {
332 + inputValue.value = ''
333 + }
334 + validateInput()
335 +}
336 +
337 +/**
338 + * 打开输入弹窗
339 + */
340 +const openInput = () => {
341 + emit('update:visible', true)
342 +}
343 +
344 +/**
345 + * 输入变化处理
346 + */
347 +const onInputChange = () => {
348 + validateInput()
349 +}
350 +
351 +/**
352 + * 选择快捷选项
353 + */
354 +const selectQuickOption = (option) => {
355 + inputValue.value = option
356 + validateInput()
357 +}
358 +
359 +/**
360 + * 验证输入
361 + * @returns {boolean} 是否有效
362 + */
363 +const validateInput = () => {
364 + const rules = props.validationRules || {}
365 + const value = inputValue.value.trim()
366 +
367 + // 空值
368 + if (!value) {
369 + isValid.value = false
370 + validationError.value = ''
371 + validationHint.value = `请输入${rules.min || 1}-${rules.max || 100}之间的整数,或选择快捷选项`
372 + return false
373 + }
374 +
375 + // 快捷选项(终身、一笔过等)
376 + if (quickOptions.value.includes(value)) {
377 + isValid.value = true
378 + validationError.value = ''
379 + validationHint.value = ''
380 + return true
381 + }
382 +
383 + // 数字年期验证
384 + const num = Number(value)
385 +
386 + // 检查是否为有效数字
387 + if (Number.isNaN(num)) {
388 + isValid.value = false
389 + validationError.value = '请输入数字'
390 + validationHint.value = ''
391 + return false
392 + }
393 +
394 + // 检查是否为整数(核心要求)
395 + if (!Number.isInteger(num)) {
396 + isValid.value = false
397 + validationError.value = '年期必须是整数'
398 + validationHint.value = ''
399 + return false
400 + }
401 +
402 + // 检查范围
403 + const min = rules.min ?? 1
404 + const max = rules.max ?? 100
405 +
406 + if (num < min) {
407 + isValid.value = false
408 + validationError.value = `年期不能小于${min}年`
409 + validationHint.value = ''
410 + return false
411 + }
412 +
413 + if (num > max) {
414 + isValid.value = false
415 + validationError.value = `年期不能大于${max}年`
416 + validationHint.value = ''
417 + return false
418 + }
419 +
420 + // 自定义验证器(预留扩展)
421 + if (rules.custom_validators && rules.custom_validators.length > 0) {
422 + for (const validator of rules.custom_validators) {
423 + const result = validator(value)
424 + if (result.valid === false) {
425 + isValid.value = false
426 + validationError.value = result.error || '格式不正确'
427 + validationHint.value = ''
428 + return false
429 + }
430 + }
431 + }
432 +
433 + // 验证通过
434 + isValid.value = true
435 + validationError.value = ''
436 + validationHint.value = ''
437 + return true
438 +}
439 +
440 +/**
441 + * 确认输入
442 + */
443 +const onConfirm = () => {
444 + if (!canConfirm.value) return
445 +
446 + let finalValue = inputValue.value
447 +
448 + // 如果是纯数字,添加"年"单位
449 + const num = Number(inputValue.value)
450 + if (!Number.isNaN(num) && Number.isInteger(num)) {
451 + finalValue = `${num}年`
452 + }
453 +
454 + emit('update:modelValue', finalValue)
455 + emit('confirm', finalValue)
456 + showInputModal.value = false
457 +}
458 +
459 +/**
460 + * 取消输入
461 + */
462 +const onCancel = () => {
463 + inputValue.value = ''
464 + isValid.value = false
465 + showInputModal.value = false
466 + emit('cancel')
467 +}
468 +</script>
469 +
470 +<style lang="less" scoped>
471 +/* 组件样式(如有需要可在此补充 TailwindCSS 无法覆盖的样式) */
472 +</style>
...@@ -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 = {
......