hookehuyr

refactor(plan): 删除未使用的旧版本选择器组件

- 删除 AgePicker.vue(已被 AgePickerGlobal.vue 替代)
- 删除 DatePicker.vue(已被 DatePickerGlobal.vue 替代)
- 删除 SelectPicker.vue(已被 SelectPickerGlobal.vue 替代)

所有模板文件已统一使用 Global 版本组件
<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="handleTap"
>
<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"
:z-index="9999"
:overlay="false"
:close-on-click-overlay="false"
:catch-move="true"
>
<nut-picker
v-model="pickerValue"
:columns="ageColumns"
@confirm="onConfirm"
@cancel="onCancel"
/>
</nut-popup>
</div>
</template>
<script setup>
/**
* 年龄选择器组件
*
* @description 使用 NutUI Popup + Picker 实现年龄选择
* - 显示格式:3位数字(如 018 表示 18 岁)
* - 提交格式:数字(如 18)
* - 年龄范围:0-120 岁
* @author Claude Code
* @example
* <AgePicker
* v-model="age"
* label="年龄"
* placeholder="请选择年龄"
* />
*/
import { ref, computed, watch, inject } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
const props = defineProps({
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 是否必填
* @type {boolean}
*/
required: {
type: Boolean,
default: false
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请选择年龄'
},
/**
* 绑定的值(数字)
* @type {number}
*/
modelValue: {
type: Number,
default: null
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {number} value - 选中的年龄(数字)
*/
'update:modelValue',
/**
* 值变化事件
* @event change
* @param {number} value - 选中的年龄(数字)
*/
'change',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
* 控制 Picker 显示
* @type {Ref<boolean>}
*/
const showPicker = ref(false)
/**
* Picker 选中的值
* @type {Ref<Array<number>>}
*/
const pickerValue = ref([0, 1, 8]) // 默认 018
/**
* 同步 Picker 值与 modelValue
*/
const syncPickerValue = () => {
// 如果 modelValue 有值(包括 0),则使用 modelValue,否则默认为 18
const age = (props.modelValue !== null && props.modelValue !== undefined)
? props.modelValue
: 18
// 确保 age 在 0-199 范围内
const validAge = Math.min(Math.max(0, age), 199)
const h = Math.floor(validAge / 100)
const t = Math.floor((validAge % 100) / 10)
const u = validAge % 10
pickerValue.value = [h, t, u]
}
// 监听 modelValue 变化
watch(() => props.modelValue, syncPickerValue, { immediate: true })
// 监听弹窗打开,重新同步值(防止上次取消后保留了未确认的值)
watch(showPicker, (val) => {
if (val) {
syncPickerValue()
}
})
/**
* 处理点击事件
*/
const handleTap = () => {
openPicker()
}
/**
* 打开选择器
*/
const openPicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
showPicker.value = true
}
/**
* 年龄选项(3列数字格式)
* @description 生成百位(0-1)、十位(0-9)、个位(0-9)的选项数组
* @returns {Array<Array<{text: string, value: number}>>} Picker 列格式
*/
const ageColumns = computed(() => {
// 百位: 0-1
const hundreds = [
{ text: '0', value: 0 },
{ text: '1', value: 1 }
]
// 十位: 0-9
const tens = Array.from({ length: 10 }, (_, i) => ({
text: i.toString(),
value: i
}))
// 个位: 0-9 (为了支持 10, 20 等年龄,个位必须包含 0)
// 用户需求提及第三列 1-9,但如果是 1-9 则无法选择 10, 20 等整数年龄
// 因此此处使用 0-9 以确保完整性
const units = Array.from({ length: 10 }, (_, i) => ({
text: i.toString(),
value: i
}))
return [hundreds, tens, units]
})
/**
* 显示的值(数字格式)
* @description 将数字转换为字符串显示
* @returns {string} 显示文本
*/
const displayValue = computed(() => {
return props.modelValue !== null && props.modelValue !== undefined
? props.modelValue.toString()
: ''
})
/**
* 确认选择
* @param {Object} params - Picker 返回参数
* @param {Array} params.selectedOptions - 选中的选项数组
* @param {Array} params.selectedValue - 选中的值数组
*/
const onConfirm = ({ selectedValue, selectedOptions }) => {
// 优先从 selectedOptions 获取值,因为它包含完整的选项对象
// 某些情况下 selectedValue 可能不完整或类型不一致
let h, t, u
if (selectedOptions && selectedOptions.length >= 3) {
h = selectedOptions[0]?.value
t = selectedOptions[1]?.value
u = selectedOptions[2]?.value
} else if (Array.isArray(selectedValue) && selectedValue.length >= 3) {
h = selectedValue[0]
t = selectedValue[1]
u = selectedValue[2]
}
// 确保所有位都有值(0 也是有效值)
if (h !== undefined && t !== undefined && u !== undefined) {
const age = parseInt(h) * 100 + parseInt(t) * 10 + parseInt(u)
if (!Number.isNaN(age)) {
emit('update:modelValue', age)
emit('change', age) // 触发 change 事件,供父组件监听
} else {
console.error('[AgePicker] 计算结果为 NaN', { h, t, u })
}
} else {
console.error('[AgePicker] 选中值无效', { selectedValue, selectedOptions })
}
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
</script>
<style lang="less">
/* 组件样式 */
</style>
<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="openDatePicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
{{ displayValue || placeholder }}
</span>
<IconFont name="right" size="14" color="#9CA3AF" />
</div>
<!-- DatePicker 弹窗 -->
<nut-popup position="bottom" v-model:visible="showDatePicker" :close-on-click-overlay="false">
<nut-date-picker
v-model="currentDate"
:min-date="minDate"
:max-date="maxDate"
:is-show-chinese="true"
@confirm="onConfirm"
@cancel="onCancel"
>
</nut-date-picker>
</nut-popup>
</div>
</template>
<script setup>
/**
* 日期选择器组件
*
* @description 使用 NutUI DatePicker + Popup 实现日期选择
* - 支持年龄范围限制(minAge, maxAge)
* - 格式:YYYY-MM-DD
* - 可触发自动计算年龄
* @author Claude Code
* @example
* <DatePicker
* v-model="birthday"
* label="出生年月日"
* placeholder="请选择日期"
* :min-age="0"
* :max-age="120"
* @change="onBirthdayChange"
* />
*/
import { ref, computed, watch, inject } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
const props = defineProps({
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 是否必填
* @type {boolean}
*/
required: {
type: Boolean,
default: false
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请选择日期'
},
/**
* 绑定的值(格式:YYYY-MM-DD)
* @type {string}
*/
modelValue: {
type: String,
default: ''
},
/**
* 最小年龄(用于计算最大出生日期)
* @type {number}
* @default 0
*/
minAge: {
type: Number,
default: 0
},
/**
* 最大年龄(用于计算最小出生日期)
* @type {number}
* @default 120
*/
maxAge: {
type: Number,
default: 120 }
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {string} value - 选中的日期(格式:YYYY-MM-DD)
*/
'update:modelValue',
/**
* 值变化事件(可用于触发自动计算年龄)
* @event change
* @param {string} value - 选中的日期(格式:YYYY-MM-DD)
*/
'change',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
* 控制 DatePicker 显示
*/
const showDatePicker = ref(false)
/**
* 当前选中的日期(Date 对象)
* 用于绑定给 nut-date-picker
*/
const currentDate = ref(new Date())
/**
* 打开日期选择器
* @description 打开时将传入的 modelValue 转换为 Date 对象
*/
const openDatePicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
if (props.modelValue) {
// 兼容 iOS 的日期格式 (YYYY/MM/DD)
const dateStr = props.modelValue.replace(/-/g, '/')
const date = new Date(dateStr)
if (!Number.isNaN(date.getTime())) {
currentDate.value = date
}
} else {
// 如果没有值,默认选中最小日期(通常是18岁或0岁对应的时间)
// 或者默认选中当前时间,视业务需求而定。这里默认选中当前时间。
currentDate.value = new Date()
}
showDatePicker.value = true
}
/**
* 计算最小可选日期(基于最大年龄)
* @description maxAge 岁对应的出生日期
* @example
* // maxAge = 75, 当前日期 = 2026-02-06
* // minDate() // 返回: 1951-02-06
*/
const minDate = computed(() => {
const date = new Date()
date.setFullYear(date.getFullYear() - props.maxAge)
return date
})
/**
* 计算最大可选日期(基于最小年龄)
* @description minAge 岁对应的出生日期
* @example
* // minAge = 0, 当前日期 = 2026-02-06
* // maxDate() // 返回: 2026-02-06
*/
const maxDate = computed(() => {
const date = new Date()
date.setFullYear(date.getFullYear() - props.minAge)
return date
})
/**
* 显示的值
*/
const displayValue = computed(() => {
return props.modelValue || ''
})
/**
* 确认选择
* @param {Object} { selectedValue } - DatePicker 返回的日期对象
*
* @example
* // 用户选择 2020-01-01
* onConfirm({ selectedValue: ['2020', '01', '01'] })
*/
const onConfirm = ({ selectedValue }) => {
// NutUI DatePicker confirm 事件返回 { selectedValue: [year, month, day], selectedOptions: [...] }
// 或者直接返回 Date 对象,取决于版本。
// 安全起见,我们查看 currentDate.value,它会被 v-model 更新
const date = currentDate.value
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const formattedDate = `${year}-${month}-${day}`
emit('update:modelValue', formattedDate)
emit('change', formattedDate)
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showDatePicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showDatePicker.value = false
}
</script>
<style lang="less">
/* 组件样式 */
</style>
<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"
:z-index="9999"
: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 岁)")
* - 适用于缴费年期等场景
* @author Claude Code
* @example
* <SelectPicker
* v-model="paymentPeriod"
* label="缴费年期"
* placeholder="请选择缴费年期"
* :options="['整付(0-75 岁)', '5 年(0-70 岁)']"
* />
*/
import { ref, computed, inject } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
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
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {string} value - 选中的选项
*/
'update:modelValue',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
* 控制 Picker 显示
*/
const showPicker = ref(false)
/**
* 打开选择器
*/
const openPicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
showPicker.value = true
}
/**
* 转换为 Picker 格式
* @description 将选项数组转换为 Picker 需要的格式
* @example
* // options = ['整付(0-75 岁)', '5 年(0-70 岁)']
* // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...]
*/
const pickerColumns = computed(() => {
return props.options.map(option => ({
text: option,
value: option // key 和 value 相同
}))
})
/**
* 显示的值
*/
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 !== undefined) {
emit('update:modelValue', value)
}
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
</script>
<style lang="less">
/* 组件样式 */
</style>