refactor(plan): 删除未使用的旧版本选择器组件
- 删除 AgePicker.vue(已被 AgePickerGlobal.vue 替代) - 删除 DatePicker.vue(已被 DatePickerGlobal.vue 替代) - 删除 SelectPicker.vue(已被 SelectPickerGlobal.vue 替代) 所有模板文件已统一使用 Global 版本组件
Showing
3 changed files
with
0 additions
and
759 deletions
| 1 | -<template> | ||
| 2 | - <div> | ||
| 3 | - <!-- 标签 --> | ||
| 4 | - <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center"> | ||
| 5 | - <span v-if="required" class="text-red-500 mr-1">*</span> | ||
| 6 | - <span>{{ label }}</span> | ||
| 7 | - </div> | ||
| 8 | - | ||
| 9 | - <!-- 触发区域 --> | ||
| 10 | - <div | ||
| 11 | - class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50" | ||
| 12 | - @tap="handleTap" | ||
| 13 | - > | ||
| 14 | - <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 15 | - {{ displayValue || placeholder }} | ||
| 16 | - </span> | ||
| 17 | - <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 18 | - </div> | ||
| 19 | - | ||
| 20 | - <!-- Picker 弹窗 --> | ||
| 21 | - <nut-popup | ||
| 22 | - position="bottom" | ||
| 23 | - v-model:visible="showPicker" | ||
| 24 | - :z-index="9999" | ||
| 25 | - :overlay="false" | ||
| 26 | - :close-on-click-overlay="false" | ||
| 27 | - :catch-move="true" | ||
| 28 | - > | ||
| 29 | - <nut-picker | ||
| 30 | - v-model="pickerValue" | ||
| 31 | - :columns="ageColumns" | ||
| 32 | - @confirm="onConfirm" | ||
| 33 | - @cancel="onCancel" | ||
| 34 | - /> | ||
| 35 | - </nut-popup> | ||
| 36 | - </div> | ||
| 37 | -</template> | ||
| 38 | - | ||
| 39 | -<script setup> | ||
| 40 | -/** | ||
| 41 | - * 年龄选择器组件 | ||
| 42 | - * | ||
| 43 | - * @description 使用 NutUI Popup + Picker 实现年龄选择 | ||
| 44 | - * - 显示格式:3位数字(如 018 表示 18 岁) | ||
| 45 | - * - 提交格式:数字(如 18) | ||
| 46 | - * - 年龄范围:0-120 岁 | ||
| 47 | - * @author Claude Code | ||
| 48 | - * @example | ||
| 49 | - * <AgePicker | ||
| 50 | - * v-model="age" | ||
| 51 | - * label="年龄" | ||
| 52 | - * placeholder="请选择年龄" | ||
| 53 | - * /> | ||
| 54 | - */ | ||
| 55 | -import { ref, computed, watch, inject } from 'vue' | ||
| 56 | -import IconFont from '@/components/icons/IconFont.vue' | ||
| 57 | - | ||
| 58 | -// 注入父组件提供的弹窗控制函数 | ||
| 59 | -const popupControl = inject('popupControl', null) | ||
| 60 | - | ||
| 61 | -/** | ||
| 62 | - * 组件属性 | ||
| 63 | - */ | ||
| 64 | -const props = defineProps({ | ||
| 65 | - /** | ||
| 66 | - * 标签文本 | ||
| 67 | - * @type {string} | ||
| 68 | - */ | ||
| 69 | - label: { | ||
| 70 | - type: String, | ||
| 71 | - default: '' | ||
| 72 | - }, | ||
| 73 | - | ||
| 74 | - /** | ||
| 75 | - * 是否必填 | ||
| 76 | - * @type {boolean} | ||
| 77 | - */ | ||
| 78 | - required: { | ||
| 79 | - type: Boolean, | ||
| 80 | - default: false | ||
| 81 | - }, | ||
| 82 | - | ||
| 83 | - /** | ||
| 84 | - * 占位符文本 | ||
| 85 | - * @type {string} | ||
| 86 | - */ | ||
| 87 | - placeholder: { | ||
| 88 | - type: String, | ||
| 89 | - default: '请选择年龄' | ||
| 90 | - }, | ||
| 91 | - | ||
| 92 | - /** | ||
| 93 | - * 绑定的值(数字) | ||
| 94 | - * @type {number} | ||
| 95 | - */ | ||
| 96 | - modelValue: { | ||
| 97 | - type: Number, | ||
| 98 | - default: null | ||
| 99 | - } | ||
| 100 | -}) | ||
| 101 | - | ||
| 102 | -/** | ||
| 103 | - * 组件事件 | ||
| 104 | - */ | ||
| 105 | -const emit = defineEmits([ | ||
| 106 | - /** | ||
| 107 | - * 更新值事件 | ||
| 108 | - * @event update:modelValue | ||
| 109 | - * @param {number} value - 选中的年龄(数字) | ||
| 110 | - */ | ||
| 111 | - 'update:modelValue', | ||
| 112 | - /** | ||
| 113 | - * 值变化事件 | ||
| 114 | - * @event change | ||
| 115 | - * @param {number} value - 选中的年龄(数字) | ||
| 116 | - */ | ||
| 117 | - 'change', | ||
| 118 | - /** | ||
| 119 | - * 弹窗打开事件 | ||
| 120 | - * @event open | ||
| 121 | - */ | ||
| 122 | - 'open', | ||
| 123 | - /** | ||
| 124 | - * 弹窗关闭事件 | ||
| 125 | - * @event close | ||
| 126 | - */ | ||
| 127 | - 'close' | ||
| 128 | -]) | ||
| 129 | - | ||
| 130 | -/** | ||
| 131 | - * 控制 Picker 显示 | ||
| 132 | - * @type {Ref<boolean>} | ||
| 133 | - */ | ||
| 134 | -const showPicker = ref(false) | ||
| 135 | - | ||
| 136 | -/** | ||
| 137 | - * Picker 选中的值 | ||
| 138 | - * @type {Ref<Array<number>>} | ||
| 139 | - */ | ||
| 140 | -const pickerValue = ref([0, 1, 8]) // 默认 018 | ||
| 141 | - | ||
| 142 | -/** | ||
| 143 | - * 同步 Picker 值与 modelValue | ||
| 144 | - */ | ||
| 145 | -const syncPickerValue = () => { | ||
| 146 | - // 如果 modelValue 有值(包括 0),则使用 modelValue,否则默认为 18 | ||
| 147 | - const age = (props.modelValue !== null && props.modelValue !== undefined) | ||
| 148 | - ? props.modelValue | ||
| 149 | - : 18 | ||
| 150 | - | ||
| 151 | - // 确保 age 在 0-199 范围内 | ||
| 152 | - const validAge = Math.min(Math.max(0, age), 199) | ||
| 153 | - | ||
| 154 | - const h = Math.floor(validAge / 100) | ||
| 155 | - const t = Math.floor((validAge % 100) / 10) | ||
| 156 | - const u = validAge % 10 | ||
| 157 | - | ||
| 158 | - pickerValue.value = [h, t, u] | ||
| 159 | -} | ||
| 160 | - | ||
| 161 | -// 监听 modelValue 变化 | ||
| 162 | -watch(() => props.modelValue, syncPickerValue, { immediate: true }) | ||
| 163 | - | ||
| 164 | -// 监听弹窗打开,重新同步值(防止上次取消后保留了未确认的值) | ||
| 165 | -watch(showPicker, (val) => { | ||
| 166 | - if (val) { | ||
| 167 | - syncPickerValue() | ||
| 168 | - } | ||
| 169 | -}) | ||
| 170 | - | ||
| 171 | -/** | ||
| 172 | - * 处理点击事件 | ||
| 173 | - */ | ||
| 174 | -const handleTap = () => { | ||
| 175 | - openPicker() | ||
| 176 | -} | ||
| 177 | - | ||
| 178 | -/** | ||
| 179 | - * 打开选择器 | ||
| 180 | - */ | ||
| 181 | -const openPicker = () => { | ||
| 182 | - // 调用父组件提供的 open 函数 | ||
| 183 | - if (popupControl && popupControl.open) { | ||
| 184 | - popupControl.open() | ||
| 185 | - } | ||
| 186 | - | ||
| 187 | - showPicker.value = true | ||
| 188 | -} | ||
| 189 | - | ||
| 190 | -/** | ||
| 191 | - * 年龄选项(3列数字格式) | ||
| 192 | - * @description 生成百位(0-1)、十位(0-9)、个位(0-9)的选项数组 | ||
| 193 | - * @returns {Array<Array<{text: string, value: number}>>} Picker 列格式 | ||
| 194 | - */ | ||
| 195 | -const ageColumns = computed(() => { | ||
| 196 | - // 百位: 0-1 | ||
| 197 | - const hundreds = [ | ||
| 198 | - { text: '0', value: 0 }, | ||
| 199 | - { text: '1', value: 1 } | ||
| 200 | - ] | ||
| 201 | - | ||
| 202 | - // 十位: 0-9 | ||
| 203 | - const tens = Array.from({ length: 10 }, (_, i) => ({ | ||
| 204 | - text: i.toString(), | ||
| 205 | - value: i | ||
| 206 | - })) | ||
| 207 | - | ||
| 208 | - // 个位: 0-9 (为了支持 10, 20 等年龄,个位必须包含 0) | ||
| 209 | - // 用户需求提及第三列 1-9,但如果是 1-9 则无法选择 10, 20 等整数年龄 | ||
| 210 | - // 因此此处使用 0-9 以确保完整性 | ||
| 211 | - const units = Array.from({ length: 10 }, (_, i) => ({ | ||
| 212 | - text: i.toString(), | ||
| 213 | - value: i | ||
| 214 | - })) | ||
| 215 | - | ||
| 216 | - return [hundreds, tens, units] | ||
| 217 | -}) | ||
| 218 | - | ||
| 219 | -/** | ||
| 220 | - * 显示的值(数字格式) | ||
| 221 | - * @description 将数字转换为字符串显示 | ||
| 222 | - * @returns {string} 显示文本 | ||
| 223 | - */ | ||
| 224 | -const displayValue = computed(() => { | ||
| 225 | - return props.modelValue !== null && props.modelValue !== undefined | ||
| 226 | - ? props.modelValue.toString() | ||
| 227 | - : '' | ||
| 228 | -}) | ||
| 229 | - | ||
| 230 | -/** | ||
| 231 | - * 确认选择 | ||
| 232 | - * @param {Object} params - Picker 返回参数 | ||
| 233 | - * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 234 | - * @param {Array} params.selectedValue - 选中的值数组 | ||
| 235 | - */ | ||
| 236 | -const onConfirm = ({ selectedValue, selectedOptions }) => { | ||
| 237 | - // 优先从 selectedOptions 获取值,因为它包含完整的选项对象 | ||
| 238 | - // 某些情况下 selectedValue 可能不完整或类型不一致 | ||
| 239 | - let h, t, u | ||
| 240 | - | ||
| 241 | - if (selectedOptions && selectedOptions.length >= 3) { | ||
| 242 | - h = selectedOptions[0]?.value | ||
| 243 | - t = selectedOptions[1]?.value | ||
| 244 | - u = selectedOptions[2]?.value | ||
| 245 | - } else if (Array.isArray(selectedValue) && selectedValue.length >= 3) { | ||
| 246 | - h = selectedValue[0] | ||
| 247 | - t = selectedValue[1] | ||
| 248 | - u = selectedValue[2] | ||
| 249 | - } | ||
| 250 | - | ||
| 251 | - // 确保所有位都有值(0 也是有效值) | ||
| 252 | - if (h !== undefined && t !== undefined && u !== undefined) { | ||
| 253 | - const age = parseInt(h) * 100 + parseInt(t) * 10 + parseInt(u) | ||
| 254 | - if (!Number.isNaN(age)) { | ||
| 255 | - emit('update:modelValue', age) | ||
| 256 | - emit('change', age) // 触发 change 事件,供父组件监听 | ||
| 257 | - } else { | ||
| 258 | - console.error('[AgePicker] 计算结果为 NaN', { h, t, u }) | ||
| 259 | - } | ||
| 260 | - } else { | ||
| 261 | - console.error('[AgePicker] 选中值无效', { selectedValue, selectedOptions }) | ||
| 262 | - } | ||
| 263 | - | ||
| 264 | - // 调用父组件提供的 close 函数 | ||
| 265 | - if (popupControl && popupControl.close) { | ||
| 266 | - popupControl.close() | ||
| 267 | - } | ||
| 268 | - | ||
| 269 | - showPicker.value = false | ||
| 270 | -} | ||
| 271 | - | ||
| 272 | -/** | ||
| 273 | - * 取消选择 | ||
| 274 | - */ | ||
| 275 | -const onCancel = () => { | ||
| 276 | - // 调用父组件提供的 close 函数 | ||
| 277 | - if (popupControl && popupControl.close) { | ||
| 278 | - popupControl.close() | ||
| 279 | - } | ||
| 280 | - | ||
| 281 | - showPicker.value = false | ||
| 282 | -} | ||
| 283 | -</script> | ||
| 284 | - | ||
| 285 | -<style lang="less"> | ||
| 286 | -/* 组件样式 */ | ||
| 287 | -</style> |
| 1 | -<template> | ||
| 2 | - <div> | ||
| 3 | - <!-- 标签 --> | ||
| 4 | - <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center"> | ||
| 5 | - <span v-if="required" class="text-red-500 mr-1">*</span> | ||
| 6 | - <span>{{ label }}</span> | ||
| 7 | - </div> | ||
| 8 | - | ||
| 9 | - <!-- 触发区域 --> | ||
| 10 | - <div | ||
| 11 | - class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50" | ||
| 12 | - @tap="openDatePicker" | ||
| 13 | - > | ||
| 14 | - <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 15 | - {{ displayValue || placeholder }} | ||
| 16 | - </span> | ||
| 17 | - <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 18 | - </div> | ||
| 19 | - | ||
| 20 | - <!-- DatePicker 弹窗 --> | ||
| 21 | - <nut-popup position="bottom" v-model:visible="showDatePicker" :close-on-click-overlay="false"> | ||
| 22 | - <nut-date-picker | ||
| 23 | - v-model="currentDate" | ||
| 24 | - :min-date="minDate" | ||
| 25 | - :max-date="maxDate" | ||
| 26 | - :is-show-chinese="true" | ||
| 27 | - @confirm="onConfirm" | ||
| 28 | - @cancel="onCancel" | ||
| 29 | - > | ||
| 30 | - </nut-date-picker> | ||
| 31 | - </nut-popup> | ||
| 32 | - </div> | ||
| 33 | -</template> | ||
| 34 | - | ||
| 35 | -<script setup> | ||
| 36 | -/** | ||
| 37 | - * 日期选择器组件 | ||
| 38 | - * | ||
| 39 | - * @description 使用 NutUI DatePicker + Popup 实现日期选择 | ||
| 40 | - * - 支持年龄范围限制(minAge, maxAge) | ||
| 41 | - * - 格式:YYYY-MM-DD | ||
| 42 | - * - 可触发自动计算年龄 | ||
| 43 | - * @author Claude Code | ||
| 44 | - * @example | ||
| 45 | - * <DatePicker | ||
| 46 | - * v-model="birthday" | ||
| 47 | - * label="出生年月日" | ||
| 48 | - * placeholder="请选择日期" | ||
| 49 | - * :min-age="0" | ||
| 50 | - * :max-age="120" | ||
| 51 | - * @change="onBirthdayChange" | ||
| 52 | - * /> | ||
| 53 | - */ | ||
| 54 | -import { ref, computed, watch, inject } from 'vue' | ||
| 55 | -import IconFont from '@/components/icons/IconFont.vue' | ||
| 56 | - | ||
| 57 | -// 注入父组件提供的弹窗控制函数 | ||
| 58 | -const popupControl = inject('popupControl', null) | ||
| 59 | - | ||
| 60 | -/** | ||
| 61 | - * 组件属性 | ||
| 62 | - */ | ||
| 63 | -const props = defineProps({ | ||
| 64 | - /** | ||
| 65 | - * 标签文本 | ||
| 66 | - * @type {string} | ||
| 67 | - */ | ||
| 68 | - label: { | ||
| 69 | - type: String, | ||
| 70 | - default: '' | ||
| 71 | - }, | ||
| 72 | - | ||
| 73 | - /** | ||
| 74 | - * 是否必填 | ||
| 75 | - * @type {boolean} | ||
| 76 | - */ | ||
| 77 | - required: { | ||
| 78 | - type: Boolean, | ||
| 79 | - default: false | ||
| 80 | - }, | ||
| 81 | - | ||
| 82 | - /** | ||
| 83 | - * 占位符文本 | ||
| 84 | - * @type {string} | ||
| 85 | - */ | ||
| 86 | - placeholder: { | ||
| 87 | - type: String, | ||
| 88 | - default: '请选择日期' | ||
| 89 | - }, | ||
| 90 | - | ||
| 91 | - /** | ||
| 92 | - * 绑定的值(格式:YYYY-MM-DD) | ||
| 93 | - * @type {string} | ||
| 94 | - */ | ||
| 95 | - modelValue: { | ||
| 96 | - type: String, | ||
| 97 | - default: '' | ||
| 98 | - }, | ||
| 99 | - | ||
| 100 | - /** | ||
| 101 | - * 最小年龄(用于计算最大出生日期) | ||
| 102 | - * @type {number} | ||
| 103 | - * @default 0 | ||
| 104 | - */ | ||
| 105 | - minAge: { | ||
| 106 | - type: Number, | ||
| 107 | - default: 0 | ||
| 108 | - }, | ||
| 109 | - | ||
| 110 | - /** | ||
| 111 | - * 最大年龄(用于计算最小出生日期) | ||
| 112 | - * @type {number} | ||
| 113 | - * @default 120 | ||
| 114 | - */ | ||
| 115 | - maxAge: { | ||
| 116 | - type: Number, | ||
| 117 | - default: 120 } | ||
| 118 | -}) | ||
| 119 | - | ||
| 120 | -/** | ||
| 121 | - * 组件事件 | ||
| 122 | - */ | ||
| 123 | -const emit = defineEmits([ | ||
| 124 | - /** | ||
| 125 | - * 更新值事件 | ||
| 126 | - * @event update:modelValue | ||
| 127 | - * @param {string} value - 选中的日期(格式:YYYY-MM-DD) | ||
| 128 | - */ | ||
| 129 | - 'update:modelValue', | ||
| 130 | - | ||
| 131 | - /** | ||
| 132 | - * 值变化事件(可用于触发自动计算年龄) | ||
| 133 | - * @event change | ||
| 134 | - * @param {string} value - 选中的日期(格式:YYYY-MM-DD) | ||
| 135 | - */ | ||
| 136 | - 'change', | ||
| 137 | - /** | ||
| 138 | - * 弹窗打开事件 | ||
| 139 | - * @event open | ||
| 140 | - */ | ||
| 141 | - 'open', | ||
| 142 | - /** | ||
| 143 | - * 弹窗关闭事件 | ||
| 144 | - * @event close | ||
| 145 | - */ | ||
| 146 | - 'close' | ||
| 147 | -]) | ||
| 148 | - | ||
| 149 | -/** | ||
| 150 | - * 控制 DatePicker 显示 | ||
| 151 | - */ | ||
| 152 | -const showDatePicker = ref(false) | ||
| 153 | - | ||
| 154 | -/** | ||
| 155 | - * 当前选中的日期(Date 对象) | ||
| 156 | - * 用于绑定给 nut-date-picker | ||
| 157 | - */ | ||
| 158 | -const currentDate = ref(new Date()) | ||
| 159 | - | ||
| 160 | -/** | ||
| 161 | - * 打开日期选择器 | ||
| 162 | - * @description 打开时将传入的 modelValue 转换为 Date 对象 | ||
| 163 | - */ | ||
| 164 | -const openDatePicker = () => { | ||
| 165 | - // 调用父组件提供的 open 函数 | ||
| 166 | - if (popupControl && popupControl.open) { | ||
| 167 | - popupControl.open() | ||
| 168 | - } | ||
| 169 | - | ||
| 170 | - if (props.modelValue) { | ||
| 171 | - // 兼容 iOS 的日期格式 (YYYY/MM/DD) | ||
| 172 | - const dateStr = props.modelValue.replace(/-/g, '/') | ||
| 173 | - const date = new Date(dateStr) | ||
| 174 | - if (!Number.isNaN(date.getTime())) { | ||
| 175 | - currentDate.value = date | ||
| 176 | - } | ||
| 177 | - } else { | ||
| 178 | - // 如果没有值,默认选中最小日期(通常是18岁或0岁对应的时间) | ||
| 179 | - // 或者默认选中当前时间,视业务需求而定。这里默认选中当前时间。 | ||
| 180 | - currentDate.value = new Date() | ||
| 181 | - } | ||
| 182 | - showDatePicker.value = true | ||
| 183 | -} | ||
| 184 | - | ||
| 185 | -/** | ||
| 186 | - * 计算最小可选日期(基于最大年龄) | ||
| 187 | - * @description maxAge 岁对应的出生日期 | ||
| 188 | - * @example | ||
| 189 | - * // maxAge = 75, 当前日期 = 2026-02-06 | ||
| 190 | - * // minDate() // 返回: 1951-02-06 | ||
| 191 | - */ | ||
| 192 | -const minDate = computed(() => { | ||
| 193 | - const date = new Date() | ||
| 194 | - date.setFullYear(date.getFullYear() - props.maxAge) | ||
| 195 | - return date | ||
| 196 | -}) | ||
| 197 | - | ||
| 198 | -/** | ||
| 199 | - * 计算最大可选日期(基于最小年龄) | ||
| 200 | - * @description minAge 岁对应的出生日期 | ||
| 201 | - * @example | ||
| 202 | - * // minAge = 0, 当前日期 = 2026-02-06 | ||
| 203 | - * // maxDate() // 返回: 2026-02-06 | ||
| 204 | - */ | ||
| 205 | -const maxDate = computed(() => { | ||
| 206 | - const date = new Date() | ||
| 207 | - date.setFullYear(date.getFullYear() - props.minAge) | ||
| 208 | - return date | ||
| 209 | -}) | ||
| 210 | - | ||
| 211 | -/** | ||
| 212 | - * 显示的值 | ||
| 213 | - */ | ||
| 214 | -const displayValue = computed(() => { | ||
| 215 | - return props.modelValue || '' | ||
| 216 | -}) | ||
| 217 | - | ||
| 218 | -/** | ||
| 219 | - * 确认选择 | ||
| 220 | - * @param {Object} { selectedValue } - DatePicker 返回的日期对象 | ||
| 221 | - * | ||
| 222 | - * @example | ||
| 223 | - * // 用户选择 2020-01-01 | ||
| 224 | - * onConfirm({ selectedValue: ['2020', '01', '01'] }) | ||
| 225 | - */ | ||
| 226 | -const onConfirm = ({ selectedValue }) => { | ||
| 227 | - // NutUI DatePicker confirm 事件返回 { selectedValue: [year, month, day], selectedOptions: [...] } | ||
| 228 | - // 或者直接返回 Date 对象,取决于版本。 | ||
| 229 | - // 安全起见,我们查看 currentDate.value,它会被 v-model 更新 | ||
| 230 | - | ||
| 231 | - const date = currentDate.value | ||
| 232 | - const year = date.getFullYear() | ||
| 233 | - const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| 234 | - const day = String(date.getDate()).padStart(2, '0') | ||
| 235 | - | ||
| 236 | - const formattedDate = `${year}-${month}-${day}` | ||
| 237 | - emit('update:modelValue', formattedDate) | ||
| 238 | - emit('change', formattedDate) | ||
| 239 | - | ||
| 240 | - // 调用父组件提供的 close 函数 | ||
| 241 | - if (popupControl && popupControl.close) { | ||
| 242 | - popupControl.close() | ||
| 243 | - } | ||
| 244 | - | ||
| 245 | - showDatePicker.value = false | ||
| 246 | -} | ||
| 247 | - | ||
| 248 | -/** | ||
| 249 | - * 取消选择 | ||
| 250 | - */ | ||
| 251 | -const onCancel = () => { | ||
| 252 | - // 调用父组件提供的 close 函数 | ||
| 253 | - if (popupControl && popupControl.close) { | ||
| 254 | - popupControl.close() | ||
| 255 | - } | ||
| 256 | - | ||
| 257 | - showDatePicker.value = false | ||
| 258 | -} | ||
| 259 | -</script> | ||
| 260 | - | ||
| 261 | -<style lang="less"> | ||
| 262 | -/* 组件样式 */ | ||
| 263 | -</style> |
| 1 | -<template> | ||
| 2 | - <div> | ||
| 3 | - <!-- 标签 --> | ||
| 4 | - <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center"> | ||
| 5 | - <span v-if="required" class="text-red-500 mr-1">*</span> | ||
| 6 | - <span>{{ label }}</span> | ||
| 7 | - </div> | ||
| 8 | - | ||
| 9 | - <!-- 触发区域 --> | ||
| 10 | - <div | ||
| 11 | - class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50" | ||
| 12 | - @tap="openPicker" | ||
| 13 | - > | ||
| 14 | - <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | ||
| 15 | - {{ displayValue || placeholder }} | ||
| 16 | - </span> | ||
| 17 | - <IconFont name="right" size="14" color="#9CA3AF" /> | ||
| 18 | - </div> | ||
| 19 | - | ||
| 20 | - <!-- Picker 弹窗 --> | ||
| 21 | - <nut-popup | ||
| 22 | - position="bottom" | ||
| 23 | - v-model:visible="showPicker" | ||
| 24 | - :z-index="9999" | ||
| 25 | - :overlay="true" | ||
| 26 | - :close-on-click-overlay="false" | ||
| 27 | - > | ||
| 28 | - <nut-picker | ||
| 29 | - :columns="pickerColumns" | ||
| 30 | - @confirm="onConfirm" | ||
| 31 | - @cancel="onCancel" | ||
| 32 | - /> | ||
| 33 | - </nut-popup> | ||
| 34 | - </div> | ||
| 35 | -</template> | ||
| 36 | - | ||
| 37 | -<script setup> | ||
| 38 | -/** | ||
| 39 | - * 下拉选择器组件 | ||
| 40 | - * | ||
| 41 | - * @description 使用 NutUI Picker 实现下拉选择功能 | ||
| 42 | - * - key 和 value 相同(如"整付(0-75 岁)") | ||
| 43 | - * - 适用于缴费年期等场景 | ||
| 44 | - * @author Claude Code | ||
| 45 | - * @example | ||
| 46 | - * <SelectPicker | ||
| 47 | - * v-model="paymentPeriod" | ||
| 48 | - * label="缴费年期" | ||
| 49 | - * placeholder="请选择缴费年期" | ||
| 50 | - * :options="['整付(0-75 岁)', '5 年(0-70 岁)']" | ||
| 51 | - * /> | ||
| 52 | - */ | ||
| 53 | -import { ref, computed, inject } from 'vue' | ||
| 54 | -import IconFont from '@/components/icons/IconFont.vue' | ||
| 55 | - | ||
| 56 | -// 注入父组件提供的弹窗控制函数 | ||
| 57 | -const popupControl = inject('popupControl', null) | ||
| 58 | - | ||
| 59 | -/** | ||
| 60 | - * 组件属性 | ||
| 61 | - */ | ||
| 62 | -const props = defineProps({ | ||
| 63 | - /** | ||
| 64 | - * 标签文本 | ||
| 65 | - * @type {string} | ||
| 66 | - */ | ||
| 67 | - label: { | ||
| 68 | - type: String, | ||
| 69 | - default: '' | ||
| 70 | - }, | ||
| 71 | - | ||
| 72 | - /** | ||
| 73 | - * 是否必填 | ||
| 74 | - * @type {boolean} | ||
| 75 | - */ | ||
| 76 | - required: { | ||
| 77 | - type: Boolean, | ||
| 78 | - default: false | ||
| 79 | - }, | ||
| 80 | - | ||
| 81 | - /** | ||
| 82 | - * 占位符文本 | ||
| 83 | - * @type {string} | ||
| 84 | - */ | ||
| 85 | - placeholder: { | ||
| 86 | - type: String, | ||
| 87 | - default: '请选择' | ||
| 88 | - }, | ||
| 89 | - | ||
| 90 | - /** | ||
| 91 | - * 绑定的值 | ||
| 92 | - * @type {string} | ||
| 93 | - */ | ||
| 94 | - modelValue: { | ||
| 95 | - type: String, | ||
| 96 | - default: '' | ||
| 97 | - }, | ||
| 98 | - | ||
| 99 | - /** | ||
| 100 | - * 选项数组(key 和 value 相同) | ||
| 101 | - * @type {Array<string>} | ||
| 102 | - * @example ['整付(0-75 岁)', '5 年(0-70 岁)', '10 年(0-70 岁)'] | ||
| 103 | - */ | ||
| 104 | - options: { | ||
| 105 | - type: Array, | ||
| 106 | - required: true | ||
| 107 | - } | ||
| 108 | -}) | ||
| 109 | - | ||
| 110 | -/** | ||
| 111 | - * 组件事件 | ||
| 112 | - */ | ||
| 113 | -const emit = defineEmits([ | ||
| 114 | - /** | ||
| 115 | - * 更新值事件 | ||
| 116 | - * @event update:modelValue | ||
| 117 | - * @param {string} value - 选中的选项 | ||
| 118 | - */ | ||
| 119 | - 'update:modelValue', | ||
| 120 | - /** | ||
| 121 | - * 弹窗打开事件 | ||
| 122 | - * @event open | ||
| 123 | - */ | ||
| 124 | - 'open', | ||
| 125 | - /** | ||
| 126 | - * 弹窗关闭事件 | ||
| 127 | - * @event close | ||
| 128 | - */ | ||
| 129 | - 'close' | ||
| 130 | -]) | ||
| 131 | - | ||
| 132 | -/** | ||
| 133 | - * 控制 Picker 显示 | ||
| 134 | - */ | ||
| 135 | -const showPicker = ref(false) | ||
| 136 | - | ||
| 137 | -/** | ||
| 138 | - * 打开选择器 | ||
| 139 | - */ | ||
| 140 | -const openPicker = () => { | ||
| 141 | - // 调用父组件提供的 open 函数 | ||
| 142 | - if (popupControl && popupControl.open) { | ||
| 143 | - popupControl.open() | ||
| 144 | - } | ||
| 145 | - | ||
| 146 | - showPicker.value = true | ||
| 147 | -} | ||
| 148 | - | ||
| 149 | -/** | ||
| 150 | - * 转换为 Picker 格式 | ||
| 151 | - * @description 将选项数组转换为 Picker 需要的格式 | ||
| 152 | - * @example | ||
| 153 | - * // options = ['整付(0-75 岁)', '5 年(0-70 岁)'] | ||
| 154 | - * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...] | ||
| 155 | - */ | ||
| 156 | -const pickerColumns = computed(() => { | ||
| 157 | - return props.options.map(option => ({ | ||
| 158 | - text: option, | ||
| 159 | - value: option // key 和 value 相同 | ||
| 160 | - })) | ||
| 161 | -}) | ||
| 162 | - | ||
| 163 | -/** | ||
| 164 | - * 显示的值 | ||
| 165 | - */ | ||
| 166 | -const displayValue = computed(() => { | ||
| 167 | - return props.modelValue || '' | ||
| 168 | -}) | ||
| 169 | - | ||
| 170 | -/** | ||
| 171 | - * 确认选择 | ||
| 172 | - * @param {Object} params - Picker 返回参数 | ||
| 173 | - * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 174 | - * | ||
| 175 | - * @example | ||
| 176 | - * // 用户选择 '整付(0-75 岁)' | ||
| 177 | - * onConfirm({ selectedOptions: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }] }) | ||
| 178 | - * // -> emit('update:modelValue', '整付(0-75 岁)') | ||
| 179 | - */ | ||
| 180 | -const onConfirm = ({ selectedOptions }) => { | ||
| 181 | - const value = selectedOptions[0]?.value | ||
| 182 | - if (value !== undefined) { | ||
| 183 | - emit('update:modelValue', value) | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - // 调用父组件提供的 close 函数 | ||
| 187 | - if (popupControl && popupControl.close) { | ||
| 188 | - popupControl.close() | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - showPicker.value = false | ||
| 192 | -} | ||
| 193 | - | ||
| 194 | -/** | ||
| 195 | - * 取消选择 | ||
| 196 | - */ | ||
| 197 | -const onCancel = () => { | ||
| 198 | - // 调用父组件提供的 close 函数 | ||
| 199 | - if (popupControl && popupControl.close) { | ||
| 200 | - popupControl.close() | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - showPicker.value = false | ||
| 204 | -} | ||
| 205 | -</script> | ||
| 206 | - | ||
| 207 | -<style lang="less"> | ||
| 208 | -/* 组件样式 */ | ||
| 209 | -</style> |
-
Please register or login to post a comment