用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器

先看效果:

关注我,带你造轮子

废话少说,直接上代码:

Calendar.vue

javascript 复制代码
<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 = startDay

    while (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 false
    const end = props.selectedEnd || props.hoverDate
    return 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

javascript 复制代码
<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 = date
        endDate.value = null
    } else {
        if (date.isBefore(startDate.value)) {
            endDate.value = startDate.value
            startDate.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.value
    if (showPicker.value) {
        nextTick(() => {
            updatePopupPosition()
        })
    }
}

const updatePopupPosition = () => {
    if (!container.value || !popup.value) return

    const containerRect = container.value.getBoundingClientRect()
    const popupRect = popup.value.getBoundingClientRect()

    const viewportHeight = window.innerHeight
    const spaceAbove = containerRect.top
    const spaceBelow = viewportHeight - containerRect.bottom

    let 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.innerWidth
    if (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

javascript 复制代码
<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这两个工具库

相关推荐
customer085 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
清灵xmf6 分钟前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据12 分钟前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_3901617721 分钟前
防抖函数--应用场景及示例
前端·javascript
334554321 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx