|
|
<script lang="ts" setup> import { computed, defineEmits, defineProps, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({ modelValue: { type: [String, Date, null], default: null, }, minDate: { type: [String, Date], default: null, }, maxDate: { type: [String, Date], default: null, }, format: { type: String, default: 'YYYY-MM-DD HH:mm:ss', }, })
const emits = defineEmits(['update:modelValue', 'change']) const dateText = ref() const hoursOptions = ref(Array.from({ length: 24 }, (_, i) => i)) const minuteOptions = ref(Array.from({ length: 60 }, (_, i) => i)) const secondOptions = ref(Array.from({ length: 60 }, (_, i) => i)) // 初始化日期时间
const selectedDate = ref<Date | null>(null) const currentYear = ref(0) const currentMonth = ref(0) const today = new Date() const weekdays = ['日', '一', '二', '三', '四', '五', '六'] const isDropdownVisible = ref(false) const selectedHour = ref('00') const selectedMinute = ref('00') const selectedSecond = ref('00')
// 格式化显示值
const displayValue = () => { if (!selectedDate.value) { return '' } const year = selectedDate.value.getFullYear() const month = (selectedDate.value.getMonth() + 1).toString().padStart(2, '0') const date = selectedDate.value.getDate().toString().padStart(2, '0') const hour = selectedDate.value.getHours().toString().padStart(2, '0') const minute = selectedDate.value.getMinutes().toString().padStart(2, '0') const second = selectedDate.value.getSeconds().toString().padStart(2, '0') dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}` }
watch(selectedDate, (newVal) => { if (newVal) { const year = newVal.getFullYear() const month = (newVal.getMonth() + 1).toString().padStart(2, '0') const date = newVal.getDate().toString().padStart(2, '0') const hour = newVal.getHours().toString().padStart(2, '0') const minute = newVal.getMinutes().toString().padStart(2, '0') const second = newVal.getSeconds().toString().padStart(2, '0') dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}` } })
// 计算当前月份的天数及上个月和下个月的部分天数
const daysInMonth: any = computed(() => { const days: Record<string, any>[] = []
// 获取当前月份第一天是星期几
const firstDay = new Date(currentYear.value, currentMonth.value, 1).getDay()
// 获取上个月的最后几天
const prevMonthLastDay = new Date(currentYear.value, currentMonth.value, 0).getDate() for (let i = firstDay; i > 0; i--) { const date = new Date(currentYear.value, currentMonth.value - 1, prevMonthLastDay - i + 1) days.push({ date, month: currentMonth.value - 1 }) }
// 获取当前月份的所有天数
const currentMonthDays = new Date(currentYear.value, currentMonth.value + 1, 0).getDate() for (let i = 1; i <= currentMonthDays; i++) { const date = new Date(currentYear.value, currentMonth.value, i) days.push({ date, month: currentMonth.value }) }
// 获取下个月的前几天
const remainingDays = 42 - days.length // 6行7列共42个格子
for (let i = 1; i <= remainingDays; i++) { const date = new Date(currentYear.value, currentMonth.value + 1, i) days.push({ date, month: currentMonth.value + 1 }) }
return days })
// 初始化
onMounted(() => { const initialDate = props.modelValue ? new Date(Number(props.modelValue)) : new Date() selectedDate.value = initialDate currentYear.value = initialDate.getFullYear() currentMonth.value = initialDate.getMonth() selectedHour.value = initialDate.getHours().toString().padStart(2, '0') selectedMinute.value = initialDate.getMinutes().toString().padStart(2, '0') selectedSecond.value = initialDate.getSeconds().toString().padStart(2, '0') })
// 监听props变化
watch(() => props.modelValue, (newVal) => { if (newVal) { const date = new Date(Number(newVal)) if (!Number.isNaN(date.getTime())) { selectedDate.value = date currentYear.value = date.getFullYear() currentMonth.value = date.getMonth() selectedHour.value = date.getHours().toString().padStart(2, '0') selectedMinute.value = date.getMinutes().toString().padStart(2, '0') selectedSecond.value = date.getSeconds().toString().padStart(2, '0') } } else { selectedDate.value = null } }, { deep: true })
// 日期操作
const prevMonth = () => { if (currentMonth.value === 0) { currentMonth.value = 11 currentYear.value-- } else { currentMonth.value-- } }
const nextMonth = () => { if (currentMonth.value === 11) { currentMonth.value = 0 currentYear.value++ } else { currentMonth.value++ } }
const prevYear = () => { currentYear.value-- }
const nextYear = () => { currentYear.value++ }
// 日期选择
const selectDate = (date: Date) => { if (!isDateInRange(date)) { return }
if (!selectedDate.value) { selectedDate.value = new Date(date) } else { // 保留原来的时分秒
selectedDate.value.setFullYear(date.getFullYear()) selectedDate.value.setMonth(date.getMonth()) selectedDate.value.setDate(date.getDate()) } displayValue() }
// 时间更新
const updateTime = () => { console.log('selectedDate.value--', selectedDate.value, selectedHour.value) if (selectedDate.value) { selectedDate.value.setHours(Number(selectedHour.value)) selectedDate.value.setMinutes(Number(selectedMinute.value)) selectedDate.value.setSeconds(Number(selectedSecond.value)) } displayValue() }
// 确认选择
const confirmSelection = () => { if (selectedDate.value) { const dateValue = formatDate(selectedDate.value, props.format) // emits('update:modelValue', dateValue)
emits('change', dateValue) } isDropdownVisible.value = false }
// 日期格式化工具函数
const formatDate = (date: Date, format: string): string => { if (!date) { return '' }
const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return format .replace('YYYY', year.toString()) .replace('MM', month.toString().padStart(2, '0')) .replace('DD', day.toString().padStart(2, '0')) .replace('HH', hour.toString().padStart(2, '0')) .replace('mm', minute.toString().padStart(2, '0')) .replace('ss', second.toString().padStart(2, '0')) }
// 取消选择
const cancelSelection = () => { // 恢复之前的值
// if (props.modelValue) {
// const date = new Date(props.modelValue)
// if (!isNaN(date.getTime())) {
// selectedDate.value = date
// selectedHour.value = date.getHours().toString().padStart(2, '0')
// selectedMinute.value = date.getMinutes().toString().padStart(2, '0')
// selectedSecond.value = date.getSeconds().toString().padStart(2, '0')
// }
// } else {
// selectedDate.value = null
// }
isDropdownVisible.value = false }
// 切换下拉框显示
const toggleDropdown = (event?: MouseEvent) => { if (event) { event.stopPropagation() } isDropdownVisible.value = !isDropdownVisible.value }
// 工具函数
const isSameDay = (date1: Date, date2: Date | null) => { if (!date2) { return false } return ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ) }
const isDateInRange = (date: Date) => { const min = props.minDate ? new Date(props.minDate) : null const max = props.maxDate ? new Date(props.maxDate) : null
if (min && date < min) { return false } if (max && date > max) { return false } return true }
// 点击外部关闭下拉框
const closeDropdownOnClickOutside = (event: MouseEvent) => { if (!isDropdownVisible.value) { return }
const target = event.target as HTMLElement const container = document.querySelector('.date-time-picker')
if (container && !container.contains(target)) { isDropdownVisible.value = false } }
// 添加和移除事件监听
onMounted(() => { document.addEventListener('click', closeDropdownOnClickOutside) })
onUnmounted(() => { document.removeEventListener('click', closeDropdownOnClickOutside) }) </script>
<template> <div class="date-time-picker"> <div class="input-group"> <input v-model="dateText" type="text" readonly placeholder="请选择日期时间" class="form-input" @click="toggleDropdown" > <span class="input-icon" @click="toggleDropdown"> <i class="fa fa-calendar" /> </span> </div> <transition name="fade"> <div v-show="isDropdownVisible" class="dropdown-container"> <div class="calendar-header"> <button class="nav-btn" @click="prevYear"> <el-icon><DArrowLeft /></el-icon> </button> <button class="nav-btn" @click="prevMonth"> <el-icon><ArrowLeft /></el-icon> </button> <span class="current-date"> {{ currentYear }}年 {{ currentMonth + 1 }}月 </span> <button class="nav-btn" @click="nextMonth"> <el-icon><ArrowRight /></el-icon> </button> <button class="nav-btn" @click="nextYear"> <el-icon><DArrowRight /></el-icon> </button> </div> <div class="weekdays"> <div v-for="day in weekdays" :key="day" class="weekday"> {{ day }} </div> </div> <div class="days"> <div v-for="(day, index) in daysInMonth" :key="index" class="day" :class="{ 'other-month': day.month !== currentMonth, 'selected': isSameDay(day.date, selectedDate), 'today': isSameDay(day.date, today), 'disabled': !isDateInRange(day.date), }" @click="selectDate(day.date)" > {{ day.date.getDate() }} </div> </div> <div class="time-selector"> <div class="time-label"> 时间: </div> <el-select v-model="selectedHour" class="time-select" @change="updateTime"> <el-option v-for="i in hoursOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;"> {{ i.toString().padStart(2, '0') }} </el-option> </el-select> <span class="time-separator">:</span> <el-select v-model="selectedMinute" class="time-select" @change="updateTime"> <el-option v-for="i in minuteOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;"> {{ i.toString().padStart(2, '0') }} </el-option> </el-select> <span class="time-separator">:</span> <el-select v-model="selectedSecond" class="time-select" @change="updateTime"> <el-option v-for="i in secondOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;"> {{ i.toString().padStart(2, '0') }} </el-option> </el-select> </div> <div class="calendar-footer"> <button class="confirm-btn" @click="confirmSelection"> 确定 </button> <button class="cancel-btn" @click="cancelSelection"> 取消 </button> </div> </div> </transition> </div> </template>
<style lang="scss" scoped> $fontSize: 1.5rem; :deep(body) { --el-font-size-base: 20px; } .date-time-picker { position: relative; width: 340px; }
.input-group { display: flex; position: relative; }
.form-input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: $fontSize; outline: none; transition: border-color 0.2s; }
.form-input:focus { border-color: #4f46e5; }
.input-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: #94a3b8; cursor: pointer; font-size: $fontSize; }
.dropdown-container { position: absolute; top: calc(100% + 4px); right: 0; width: 28rem; background-color: white; border: 1px solid #e2e8f0; border-radius: 4px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); z-index: 100; padding: 12px; }
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.nav-btn { background: none; border: none; cursor: pointer; color: #64748b; font-size: $fontSize; padding: 6px; border-radius: 4px; transition: background-color 0.2s; }
.nav-btn:hover { background-color: #f1f5f9; }
.current-date { font-weight: 500; color: #334155; font-size: $fontSize; }
.weekdays { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: 6px; }
.weekday { text-align: center; font-size: $fontSize; color: #64748b; padding: 6px; }
.days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 12px; }
.day { text-align: center; padding: 8px; border-radius: 4px; font-size: $fontSize; cursor: pointer; transition: background-color 0.2s; }
.day:hover:not(.disabled) { background-color: #e0f2fe; }
.day.other-month { color: #94a3b8; }
.day.selected { background-color: #3b82f6; color: white; }
.day.today:not(.selected) { font-weight: 500; color: #3b82f6; }
.day.disabled { color: #cbd5e1; cursor: not-allowed; }
.time-selector { display: flex; align-items: center; margin-bottom: 12px; .time-select { padding: 4px 8px; border-radius: 4px; font-size: 20px; outline: none; background-color: white; margin-right: 4px; /* 最大高度限制,溢出滚动 */ max-height: 3rem; width: 8rem; border: none; } }
select option { max-height: 3rem; border: 1px solid red; }
.time-label { margin-right: 12px; font-size: $fontSize; color: #334155; width: 5rem; }
.select-option{ max-height: 3rem; }
select { padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: $fontSize; outline: none; background-color: white; margin-right: 6px; width: 60px; }
.time-separator { margin: 0 3px; font-size: $fontSize; color: #334155; }
.calendar-footer { display: flex; justify-content: flex-end; gap: 10px; }
.confirm-btn, .cancel-btn { padding: 8px 16px; border: none; border-radius: 4px; font-size: $fontSize; cursor: pointer; transition: background-color 0.2s; }
.confirm-btn { background-color: #3b82f6; color: white; }
.confirm-btn:hover { background-color: #2563eb; }
.cancel-btn { background-color: #f1f5f9; color: #334155; }
.cancel-btn:hover { background-color: #e2e8f0; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s; }
.fade-enter-from, .fade-leave-to { opacity: 0; } // 修改选择框本身的宽高
:deep(.el-select__wrapper){ height: 3rem; font-size: 20px; } </style>
|