447 lines
11 KiB
Vue
447 lines
11 KiB
Vue
<template>
|
||
<div class="base-date-time-picker">
|
||
<!-- 使用 van-field 作为展示区域 - 修复 bug1:改为 modelValue -->
|
||
<van-field
|
||
v-model="displayValue"
|
||
:label="label"
|
||
:required="required"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
:readonly="true"
|
||
:right-icon="rightIcon"
|
||
:clickable="!disabled"
|
||
@click="openPicker"
|
||
/>
|
||
|
||
<!-- 弹出层:同时包含日期和时间选择器 -->
|
||
<van-popup v-model:show="showPicker" position="bottom">
|
||
<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'
|
||
import { formatDate } from '@shared/utils'
|
||
|
||
// Props 定义
|
||
const props = defineProps({
|
||
// 双向绑定值 (v-model)
|
||
modelValue: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 左侧标签文字
|
||
label: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
required: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 占位符
|
||
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
|
||
}
|
||
},
|
||
// 返回结果格式化
|
||
resultFormat: {
|
||
type: String,
|
||
default: 'YYYY-MM-DD HH:mm:ss'
|
||
},
|
||
// 选项过滤函数
|
||
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
|
||
}
|
||
}
|
||
|
||
finalValue = formatDate(finalValue)
|
||
|
||
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>
|