Compare commits
2 Commits
3e7c9dd1af
...
63fc4898d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 63fc4898d1 | |||
| 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