Compare commits

...

2 Commits

View 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>