feat: 日期选择器
This commit is contained in:
parent
d578797f5b
commit
f854f8d057
434
packages/mobile/src/components/BaseDatePicker.vue
Normal file
434
packages/mobile/src/components/BaseDatePicker.vue
Normal file
@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<div class="base-date-time-picker">
|
||||
<!-- 使用 van-field 作为展示区域 - 修复 bug1:改为 modelValue -->
|
||||
<van-field
|
||||
v-model="displayValue"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="true"
|
||||
:right-icon="rightIcon"
|
||||
:clickable="!disabled"
|
||||
@click="openPicker"
|
||||
/>
|
||||
|
||||
<!-- 弹出层:同时包含日期和时间选择器 -->
|
||||
<van-popup v-model:show="showPicker" position="bottom" round>
|
||||
<div class="picker-container">
|
||||
<div class="picker-header">
|
||||
<span class="picker-cancel" @click="showPicker = false">取消</span>
|
||||
<span class="picker-title">{{ pickerTitle }}</span>
|
||||
<span class="picker-confirm" @click="onConfirm">确定</span>
|
||||
</div>
|
||||
|
||||
<div class="picker-wrapper" :class="{ 'has-time': hasTime }">
|
||||
<!-- 日期选择器 -->
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:columns-type="dateColumnsType"
|
||||
:formatter="formatter"
|
||||
:filter="filter"
|
||||
:loading="loading"
|
||||
:show-toolbar="false"
|
||||
/>
|
||||
|
||||
<!-- 时间选择器(当有小时列时显示) -->
|
||||
<van-time-picker
|
||||
v-if="hasTime"
|
||||
v-model="currentTime"
|
||||
:columns-type="timeColumnsType"
|
||||
:min-hour="minHour"
|
||||
:max-hour="maxHour"
|
||||
:min-minute="minMinute"
|
||||
:max-minute="maxMinute"
|
||||
:formatter="formatter"
|
||||
:show-toolbar="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Field, Popup, DatePicker, TimePicker } from 'vant'
|
||||
|
||||
// Props 定义
|
||||
const props = defineProps({
|
||||
// 双向绑定值 (v-model)
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 左侧标签文字
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 占位符
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择日期时间'
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Picker 顶部标题
|
||||
pickerTitle: {
|
||||
type: String,
|
||||
default: '选择日期时间'
|
||||
},
|
||||
// 列类型:支持 'year', 'month', 'day', 'hour', 'minute', 'second'
|
||||
columnsType: {
|
||||
type: Array,
|
||||
default: () => ['year', 'month', 'day', 'hour', 'minute']
|
||||
},
|
||||
// 最小日期
|
||||
minDate: {
|
||||
type: Date,
|
||||
default: () => new Date(1900, 0, 1)
|
||||
},
|
||||
// 最大日期
|
||||
maxDate: {
|
||||
type: Date,
|
||||
default: () => new Date(2100, 11, 31)
|
||||
},
|
||||
// 最小小时
|
||||
minHour: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大小时
|
||||
maxHour: {
|
||||
type: Number,
|
||||
default: 23
|
||||
},
|
||||
// 最小分钟
|
||||
minMinute: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大分钟
|
||||
maxMinute: {
|
||||
type: Number,
|
||||
default: 59
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 右侧图标
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: 'arrow-down'
|
||||
},
|
||||
// 日期时间格式化函数
|
||||
formatter: {
|
||||
type: Function,
|
||||
default: (type, option) => {
|
||||
if (type === 'year') option.text += '年'
|
||||
if (type === 'month') option.text += '月'
|
||||
if (type === 'day') option.text += '日'
|
||||
if (type === 'hour') option.text += '时'
|
||||
if (type === 'minute') option.text += '分'
|
||||
if (type === 'second') option.text += '秒'
|
||||
return option
|
||||
}
|
||||
},
|
||||
// 选项过滤函数
|
||||
filter: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// Emits 定义
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
// 控制弹出层显示
|
||||
const showPicker = ref(false)
|
||||
|
||||
// 当前选中的日期(数组格式,如 ['2024', '01', '01'])
|
||||
const currentDate = ref([])
|
||||
|
||||
// 当前选中的时间(数组格式,如 ['14', '30', '00'])
|
||||
const currentTime = ref([])
|
||||
|
||||
// 分离日期列类型和时间列类型
|
||||
const dateColumnsType = computed(() => {
|
||||
return props.columnsType.filter(col => ['year', 'month', 'day'].includes(col))
|
||||
})
|
||||
|
||||
const timeColumnsType = computed(() => {
|
||||
return props.columnsType.filter(col => ['hour', 'minute', 'second'].includes(col))
|
||||
})
|
||||
|
||||
// 是否有时间选择
|
||||
const hasTime = computed(() => {
|
||||
return timeColumnsType.value.length > 0
|
||||
})
|
||||
|
||||
// 格式化日期显示(修复 bug2:正确处理分钟显示)
|
||||
const displayValue = computed(() => {
|
||||
if (!props.modelValue) return ''
|
||||
|
||||
// 根据 columnsType 格式化显示
|
||||
const hasYear = props.columnsType.includes('year')
|
||||
const hasMonth = props.columnsType.includes('month')
|
||||
const hasDay = props.columnsType.includes('day')
|
||||
const hasHour = props.columnsType.includes('hour')
|
||||
const hasMinute = props.columnsType.includes('minute')
|
||||
const hasSecond = props.columnsType.includes('second')
|
||||
|
||||
// 解析日期时间
|
||||
let datePart = ''
|
||||
let timePart = ''
|
||||
|
||||
if (props.modelValue.includes(' ')) {
|
||||
[datePart, timePart] = props.modelValue.split(' ')
|
||||
} else if (props.modelValue.includes('-')) {
|
||||
datePart = props.modelValue
|
||||
} else if (props.modelValue.includes(':')) {
|
||||
timePart = props.modelValue
|
||||
}
|
||||
|
||||
// 格式化日期部分
|
||||
let result = ''
|
||||
if (datePart && (hasYear || hasMonth || hasDay)) {
|
||||
const dateArr = datePart.split('-')
|
||||
if (hasYear && dateArr[0]) result += dateArr[0]
|
||||
if (hasMonth && dateArr[1]) result += (result ? '-' : '') + dateArr[1]
|
||||
if (hasDay && dateArr[2]) result += (result ? '-' : '') + dateArr[2]
|
||||
}
|
||||
|
||||
// 格式化时间部分 - 修复 bug2:确保分钟正确显示
|
||||
if (timePart && (hasHour || hasMinute || hasSecond)) {
|
||||
const timeArr = timePart.split(':')
|
||||
let timeResult = ''
|
||||
if (hasHour && timeArr[0]) timeResult += timeArr[0]
|
||||
if (hasMinute && timeArr[1]) timeResult += (timeResult ? ':' : '') + timeArr[1]
|
||||
if (hasSecond && timeArr[2]) timeResult += (timeResult ? ':' : '') + timeArr[2]
|
||||
|
||||
if (timeResult) {
|
||||
result += (result ? ' ' : '') + timeResult
|
||||
}
|
||||
}
|
||||
|
||||
return result || props.modelValue
|
||||
})
|
||||
|
||||
// 获取默认时间值(根据列类型)
|
||||
const getDefaultTime = () => {
|
||||
const now = new Date()
|
||||
const timeValues = []
|
||||
|
||||
if (timeColumnsType.value.includes('hour')) {
|
||||
timeValues.push(String(now.getHours()).padStart(2, '0'))
|
||||
}
|
||||
if (timeColumnsType.value.includes('minute')) {
|
||||
timeValues.push(String(now.getMinutes()).padStart(2, '0'))
|
||||
}
|
||||
if (timeColumnsType.value.includes('second')) {
|
||||
timeValues.push(String(now.getSeconds()).padStart(2, '0'))
|
||||
}
|
||||
|
||||
return timeValues
|
||||
}
|
||||
|
||||
// 初始化当前值
|
||||
const initValue = () => {
|
||||
// 初始化日期部分
|
||||
if (props.modelValue) {
|
||||
// 解析 YYYY-MM-DD HH:mm:ss 格式
|
||||
let datePart = ''
|
||||
let timePart = ''
|
||||
|
||||
if (props.modelValue.includes(' ')) {
|
||||
[datePart, timePart] = props.modelValue.split(' ')
|
||||
} else if (props.modelValue.includes('-')) {
|
||||
datePart = props.modelValue
|
||||
} else if (props.modelValue.includes(':')) {
|
||||
timePart = props.modelValue
|
||||
}
|
||||
|
||||
if (datePart) {
|
||||
const dateArr = datePart.split('-')
|
||||
currentDate.value = []
|
||||
if (dateColumnsType.value.includes('year') && dateArr[0]) {
|
||||
currentDate.value.push(dateArr[0])
|
||||
}
|
||||
if (dateColumnsType.value.includes('month') && dateArr[1]) {
|
||||
currentDate.value.push(dateArr[1])
|
||||
}
|
||||
if (dateColumnsType.value.includes('day') && dateArr[2]) {
|
||||
currentDate.value.push(dateArr[2])
|
||||
}
|
||||
} else {
|
||||
const now = new Date()
|
||||
currentDate.value = []
|
||||
if (dateColumnsType.value.includes('year')) {
|
||||
currentDate.value.push(String(now.getFullYear()))
|
||||
}
|
||||
if (dateColumnsType.value.includes('month')) {
|
||||
currentDate.value.push(String(now.getMonth() + 1).padStart(2, '0'))
|
||||
}
|
||||
if (dateColumnsType.value.includes('day')) {
|
||||
currentDate.value.push(String(now.getDate()).padStart(2, '0'))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时间部分(根据列类型)
|
||||
if (hasTime.value) {
|
||||
if (timePart) {
|
||||
const timeArr = timePart.split(':')
|
||||
const result = []
|
||||
|
||||
if (timeColumnsType.value.includes('hour')) {
|
||||
result.push(timeArr[0] || '00')
|
||||
}
|
||||
if (timeColumnsType.value.includes('minute')) {
|
||||
result.push(timeArr[1] || '00')
|
||||
}
|
||||
if (timeColumnsType.value.includes('second')) {
|
||||
result.push(timeArr[2] || '00')
|
||||
}
|
||||
currentTime.value = result
|
||||
} else {
|
||||
currentTime.value = getDefaultTime()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认当前日期
|
||||
const now = new Date()
|
||||
currentDate.value = []
|
||||
if (dateColumnsType.value.includes('year')) {
|
||||
currentDate.value.push(String(now.getFullYear()))
|
||||
}
|
||||
if (dateColumnsType.value.includes('month')) {
|
||||
currentDate.value.push(String(now.getMonth() + 1).padStart(2, '0'))
|
||||
}
|
||||
if (dateColumnsType.value.includes('day')) {
|
||||
currentDate.value.push(String(now.getDate()).padStart(2, '0'))
|
||||
}
|
||||
|
||||
// 默认当前时间(根据列类型)
|
||||
if (hasTime.value) {
|
||||
currentTime.value = getDefaultTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打开选择器
|
||||
const openPicker = () => {
|
||||
if (!props.disabled) {
|
||||
initValue()
|
||||
showPicker.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期部分
|
||||
const formatDatePart = () => {
|
||||
if (!currentDate.value || currentDate.value.length === 0) return ''
|
||||
let result = currentDate.value.join('-')
|
||||
return result
|
||||
}
|
||||
|
||||
// 格式化时间部分
|
||||
const formatTimePart = () => {
|
||||
if (!hasTime.value) return ''
|
||||
if (!currentTime.value || currentTime.value.length === 0) return ''
|
||||
let result = currentTime.value.join(':')
|
||||
return result
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const onConfirm = () => {
|
||||
let finalValue = formatDatePart()
|
||||
|
||||
if (hasTime.value) {
|
||||
const timePart = formatTimePart()
|
||||
if (timePart) {
|
||||
finalValue += ' ' + timePart
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:modelValue', finalValue)
|
||||
emit('change', finalValue)
|
||||
showPicker.value = false
|
||||
}
|
||||
|
||||
// 监听外部 modelValue 变化,重新初始化
|
||||
watch(() => props.modelValue, () => {
|
||||
initValue()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.base-date-time-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.van-field__control--readonly) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.picker-container {
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
}
|
||||
|
||||
.picker-cancel {
|
||||
font-size: 14px;
|
||||
color: #969799;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
.picker-confirm {
|
||||
font-size: 14px;
|
||||
color: #1989fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.picker-wrapper .van-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 只有日期选择器时占满宽度 */
|
||||
.picker-wrapper:not(.has-time) .van-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 同时有日期和时间时调整宽度比例 */
|
||||
.picker-wrapper.has-time .van-date-picker {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.picker-wrapper.has-time .van-time-picker {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user