先看效果:
关注我,带你造轮子
废话少说,直接上代码:
Calendar.vue
<template><div class="calendar"><div class="grid grid-cols-7 mb-2"><div v-for="day in weekDays" :key="day" class="text-center text-sm text-gray-700">{{ day }}</div></div><div class="grid grid-cols-7 gap-px"><div v-for="{ date, isCurrentMonth, isInRange, isStart, isEnd } in calendarDays":key="date.format('YYYY-MM-DD')" class="relative p-1"@click="isCurrentMonth && $emit('selectDate', date)"@mouseenter="isCurrentMonth && $emit('hoverDate', date)"><button type="button" :class="['w-full h-8 text-sm leading-8 rounded-full',isCurrentMonth ? 'text-gray-900' : 'text-gray-400',{'bg-blue-500 text-white': isStart || isEnd,'bg-blue-50': isInRange,'hover:bg-gray-100': isCurrentMonth && !isStart && !isEnd && !isInRange}]" :disabled="!isCurrentMonth">{{ date.date() }}</button></div></div></div>
</template><script setup>
import { computed } from 'vue'
import dayjs from 'dayjs'const props = defineProps({currentDate: {type: Object,required: true},selectedStart: Object,selectedEnd: Object,hoverDate: Object
})const emit = defineEmits(['selectDate', 'hoverDate'])const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']const calendarDays = computed(() => {const firstDay = props.currentDate.startOf('month')const lastDay = props.currentDate.endOf('month')const startDay = firstDay.startOf('week')const endDay = lastDay.endOf('week')const days = []let day = startDaywhile (day.isBefore(endDay) || day.isSame(endDay, 'day')) {days.push({date: day,isCurrentMonth: day.month() === props.currentDate.month(),isInRange: isInRange(day),isStart: isStart(day),isEnd: isEnd(day)})day = day.add(1, 'day')}return days
})const isInRange = (date) => {if (!props.selectedStart || !props.hoverDate) return falseconst end = props.selectedEnd || props.hoverDatereturn date.isAfter(props.selectedStart) && date.isBefore(end)
}const isStart = (date) => {return props.selectedStart && date.isSame(props.selectedStart, 'day')
}const isEnd = (date) => {if (props.selectedEnd) {return date.isSame(props.selectedEnd, 'day')}return props.hoverDate && date.isSame(props.hoverDate, 'day')
}
</script>
DataPicker.vue
<template><div class="relative inline-block text-left w-full box-content " ref="container"><!-- Input Field --><div @click="togglePicker"class="w-full px-1 py-1 text-gray-500 bg-white border border-gray-300 rounded-md cursor-pointer hover:border-blue-500 focus:outline-none"><div class="flex items-center align-middle "><CalendarIcon class="w-5 h-5 mr-2 text-gray-400" /><span v-if="startDate && endDate" class="flex items-center justify-evenly w-full"><span>{{ formatDate(startDate) }}</span> <span>To</span> <span>{{ formatDate(endDate) }}</span></span><span v-else class="text-gray-400">Start Date - End Date</span></div></div><!-- Calendar Popup --><div v-if="showPicker" ref="popup" :style="popupStyle"class="absolute z-50 mt-2 bg-white rounded-lg shadow-lg p-4 border border-gray-200" style="width: 720px"><div class="flex space-x-8"><!-- Left Calendar --><div class="flex-1"><div class="flex items-center justify-between mb-4"><button class="p-1 hover:bg-gray-100 rounded-full" @click="navigateMonth('left', -12)">«</button><button class="p-1 hover:bg-gray-100 rounded-full" @click="navigateMonth('left', -1)">‹</button><span class="text-gray-700">{{ formatMonthYear(leftMonth) }}</span><div class="w-8"></div></div><Calendar :current-date="leftMonth" :selected-start="startDate" :selected-end="endDate":hover-date="hoverDate" @select-date="handleDateSelect" @hover-date="handleHoverDate" /></div><!-- Right Calendar --><div class="flex-1"><div class="flex items-center justify-between mb-4"><div class="w-8"></div><span class="text-gray-700">{{ formatMonthYear(rightMonth) }}</span><button class="p-1 hover:bg-gray-100 rounded-full" @click="navigateMonth('right', 1)">›</button><button class="p-1 hover:bg-gray-100 rounded-full" @click="navigateMonth('right', 12)">»</button></div><Calendar :current-date="rightMonth" :selected-start="startDate" :selected-end="endDate":hover-date="hoverDate" @select-date="handleDateSelect" @hover-date="handleHoverDate" /></div></div></div></div>
</template><script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import dayjs from 'dayjs'
import { CalendarIcon } from '@heroicons/vue/24/outline'
import Calendar from './Calendar.vue'// Props and emits
const props = defineProps({modelValue: {type: Array,default: () => [null, null]}
})const emit = defineEmits(['update:modelValue'])// State
const showPicker = ref(false)
const leftMonth = ref(dayjs())
const hoverDate = ref(null)
const startDate = ref(null)
const endDate = ref(null)
const container = ref(null)
const popup = ref(null)
const popupStyle = ref({})// Computed
const rightMonth = computed(() => {return leftMonth.value.add(1, 'month')
})// Methods
const formatDate = (date) => {if (!date) return ''return date.format('YYYY-MM-DD')
}const formatMonthYear = (date) => {return date.format('YYYY MMMM')
}const navigateMonth = (calendar, amount) => {leftMonth.value = leftMonth.value.add(amount, 'month')
}const handleDateSelect = (date) => {if (!startDate.value || (startDate.value && endDate.value)) {startDate.value = dateendDate.value = null} else {if (date.isBefore(startDate.value)) {endDate.value = startDate.valuestartDate.value = date} else {endDate.value = date}emit('update:modelValue', [formatDate(startDate.value), formatDate(endDate.value)])showPicker.value = false}
}const handleHoverDate = (date) => {hoverDate.value = date
}const handleClickOutside = (event) => {if (container.value && !container.value.contains(event.target)) {showPicker.value = false}
}const togglePicker = () => {showPicker.value = !showPicker.valueif (showPicker.value) {nextTick(() => {updatePopupPosition()})}
}const updatePopupPosition = () => {if (!container.value || !popup.value) returnconst containerRect = container.value.getBoundingClientRect()const popupRect = popup.value.getBoundingClientRect()const viewportHeight = window.innerHeightconst spaceAbove = containerRect.topconst spaceBelow = viewportHeight - containerRect.bottomlet top = '100%'let bottom = 'auto'let transformOrigin = 'top'if (spaceBelow < popupRect.height && spaceAbove > spaceBelow) {top = 'auto'bottom = '100%'transformOrigin = 'bottom'}let left = '0'const rightOverflow = containerRect.left + popupRect.width - window.innerWidthif (rightOverflow > 0) {left = `-${rightOverflow}px`}popupStyle.value = {top,bottom,left,transformOrigin,}
}// Lifecycle
onMounted(() => {document.addEventListener('click', handleClickOutside)window.addEventListener('resize', updatePopupPosition)window.addEventListener('scroll', updatePopupPosition)if (props.modelValue[0] && props.modelValue[1]) {startDate.value = dayjs(props.modelValue[0])endDate.value = dayjs(props.modelValue[1])leftMonth.value = startDate.value}
})onUnmounted(() => {document.removeEventListener('click', handleClickOutside)window.removeEventListener('resize', updatePopupPosition)window.removeEventListener('scroll', updatePopupPosition)
})
</script>
app.vue
<template><div class="p-4"><h1 class="text-2xl font-bold mb-4">日期选择器示例</h1><DatePicker v-model="selectedDate" format="YYYY年MM月DD日" /><p class="mt-4">选择的日期: {{ selectedDate }}</p></div>
</template><script setup>
import { ref } from 'vue'
import DatePicker from '@/components/DatePicker/DataPicker.vue'const selectedDate = ref(['', ''])
</script>
最后注意安装dayjs和@heroicons/vue这两个工具库