SelectPickerGlobal.vue
6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<template>
<div>
<!-- 标签 -->
<div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center">
<span v-if="required" class="text-red-500 mr-1">*</span>
<span>{{ label }}</span>
</div>
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="openPicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
{{ displayValue || placeholder }}
</span>
<IconFont name="right" size="14" color="#9CA3AF" />
</div>
<!-- Picker 弹窗 -->
<nut-popup
position="bottom"
v-model:visible="showPicker"
:overlay="true"
:close-on-click-overlay="false"
>
<nut-picker
:columns="pickerColumns"
@confirm="onConfirm"
@cancel="onCancel"
/>
</nut-popup>
</div>
</template>
<script setup>
/**
* 下拉选择器组件(全局弹窗管理器版本)
*
* @description 使用 NutUI Picker 实现下拉选择功能
* - key 和 value 相同(如"整付(0-75 岁)")
* - 适用于缴费年期、提取期等场景
* - 使用 GlobalPopupManager 管理弹窗层级
* - 支持自定义输入选项(可选功能)
* @author Claude Code
* @version 2.1.0 - 新增自定义输入支持
* @example
* // 基础用法
* <SelectPickerGlobal
* v-model="paymentPeriod"
* label="缴费年期"
* placeholder="请选择缴费年期"
* :options="['整付(0-75 岁)', '5 年(0-70 岁)']"
* />
*
* @example
* // 支持自定义输入
* <SelectPickerGlobal
* v-model="withdrawalPeriod"
* label="提取期"
* placeholder="请选择提取期"
* :options="['1年', '5年', '10年', '终身']"
* :allow-custom="true"
* @custom-select="openCustomInput"
* />
*/
import { ref, computed, onMounted } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
import { useGlobalPopup } from './GlobalPopupManager'
/**
* 使用全局弹窗管理器
*/
const { registerPopup, activatePopup, deactivatePopup } = useGlobalPopup()
/**
* 弹窗 ID(由 GlobalPopupManager 分配)
* @type {Ref<string|null>}
*/
const popupId = ref(null)
/**
* 组件挂载时注册弹窗
*/
onMounted(() => {
popupId.value = registerPopup()
})
/**
* 组件属性
*/
const props = defineProps({
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 是否必填
* @type {boolean}
*/
required: {
type: Boolean,
default: false
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请选择'
},
/**
* 绑定的值
* @type {string}
*/
modelValue: {
type: String,
default: ''
},
/**
* 选项数组(key 和 value 相同)
* @type {Array<string>}
* @example ['整付(0-75 岁)', '5 年(0-70 岁)', '10 年(0-70 岁)']
*/
options: {
type: Array,
required: true
},
/**
* 是否允许自定义输入
* @type {boolean}
* @description 开启后,选项列表末尾会添加"自定义输入"选项
*/
allowCustom: {
type: Boolean,
default: false
},
/**
* 自定义选项的显示文本
* @type {string}
*/
customLabel: {
type: String,
default: '📝 自定义输入...'
},
/**
* 分隔符文本
* @type {string}
* @description 自定义选项与标准选项之间的分隔线
*/
dividerLabel: {
type: String,
default: '──────────'
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {string} value - 选中的选项
*/
'update:modelValue',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close',
/**
* 用户选择自定义输入选项
* @event custom-select
*/
'custom-select'
])
/**
* 控制 Picker 显示
*/
const showPicker = ref(false)
/**
* 打开选择器
*/
const openPicker = () => {
// 激活弹窗(隐藏父弹窗底部按钮)
if (popupId.value) {
activatePopup(popupId.value)
}
showPicker.value = true
emit('open')
}
/**
* 转换为 Picker 格式
* @description 将选项数组转换为 Picker 需要的格式
* 如果允许自定义,会在末尾添加分隔符和自定义选项
* @example
* // options = ['整付(0-75 岁)', '5 年(0-70 岁)']
* // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...]
*/
const pickerColumns = computed(() => {
const standardOptions = props.options.map(option => ({
text: option,
value: option // key 和 value 相同
}))
// 如果允许自定义,添加分隔符和自定义选项
if (props.allowCustom) {
return [
...standardOptions,
{
text: props.dividerLabel,
value: '__divider__',
disabled: true // 分隔符不可选
},
{
text: props.customLabel,
value: '__custom__'
}
]
}
return standardOptions
})
/**
* 显示的值
*/
const displayValue = computed(() => {
return props.modelValue || ''
})
/**
* 确认选择
* @param {Object} params - Picker 返回参数
* @param {Array} params.selectedOptions - 选中的选项数组
*
* @example
* // 用户选择 '整付(0-75 岁)'
* onConfirm({ selectedOptions: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }] })
* // -> emit('update:modelValue', '整付(0-75 岁)')
*/
const onConfirm = ({ selectedOptions }) => {
const value = selectedOptions[0]?.value
// 处理自定义选项
if (value === '__custom__') {
// 触发自定义选择事件,由父组件处理
emit('custom-select')
// 停用弹窗
if (popupId.value) {
deactivatePopup(popupId.value)
}
showPicker.value = false
emit('close')
return
}
// 跳过分隔符
if (value === '__divider__') {
return
}
// 标准选项处理
if (value !== undefined) {
emit('update:modelValue', value)
}
// 停用弹窗(恢复父弹窗底部按钮)
if (popupId.value) {
deactivatePopup(popupId.value)
}
showPicker.value = false
emit('close')
}
/**
* 取消选择
*/
const onCancel = () => {
// 停用弹窗(恢复父弹窗底部按钮)
if (popupId.value) {
deactivatePopup(popupId.value)
}
showPicker.value = false
emit('close')
}
</script>
<style lang="less">
/* 组件样式 */
</style>