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

news/2024/11/8 21:48:58 标签: vue.js, javascript, 前端

先看效果:

在这里插入图片描述
关注我,带你造轮子
废话少说,直接上代码:
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这两个工具库


http://www.niftyadmin.cn/n/5744469.html

相关文章

QT信号和槽与自定义的信号和槽

QT信号和槽与自定义的信号和槽 1.概述 这篇文章介绍下QT信号和槽的入门知识&#xff0c;通过一个案例介绍如何创建信号和槽&#xff0c;并调用他们。 2.信号和槽使用 下面通过点击按钮关闭窗口的案例介绍如何使用信号和槽。 创建按钮 在widget.cpp文件中创建按钮代码如下 …

微服务保护相关面试题

微服务保护 思考面试题: 是否了解什么是微服务的雪崩效应? 或 微服务间如果调用失败&#xff0c;该如何处理? 微服务组件 alibaba-sentinel 介绍? 可以做什么&#xff1f; 如何基于sentinel实现限流功能&#xff1f; sentinel支持的限流规则? 什么是线程隔离? sentine…

AIDOVECL数据集:包含超过15000张AI生成的车辆图像数据集,目的解决旨在解决眼水平分类和定位问题。

2024-11-01&#xff0c;由伊利诺伊大学厄巴纳-香槟分校的研究团队创建的AIDOVECL数据集&#xff0c;通过AI生成的车辆图像&#xff0c;显著减少了手动标注工作&#xff0c;为自动驾驶、城市规划和环境监测等领域提供了丰富的眼水平车辆图像资源。 数据集地址&#xff1a;AIDOV…

数据库中的用户管理和权限管理

​ 我们进行数据库操作的地方其实是数据库的客户端&#xff0c;是我们在客户端将操作发送给数据库的服务器&#xff08;MySQL的服务器是mysqld&#xff09;&#xff0c;由数据库处理之后发送回来处理结果&#xff08;其实就是一种网络服务&#xff09;。所以可以存在多个客户端…

搭建企业私有云 只需一台设备 融合计算、存储与K8s

Infortrend老牌存储厂商推出 KS 企业私有云产品&#xff0c;将计算节点、存储与Kubernetes整合在一套系统中&#xff0c;为企业提供高效稳定的专属本地私有云平台。 KS 同时内置 Kubernetes 平台和虚拟机管理程序&#xff0c;既能运行云原生容器化应用程序&#xff0c;例如大数…

Docker安装部署单机版高斯数据库gaussdb

opengauss官网&#xff1a;https://opengauss.org/ opengauss镜像&#xff1a;https://hub.docker.com/r/enmotech/opengauss 一&#xff1a;镜像拉取并运行 如果出现镜像无法拉取的情况&#xff0c;请先在本地&#xff0c;开启VPN访问外网&#xff0c;拉取镜像&#xff0c;再…

vscode远程连接+免密登录

一、远程连接 本地主机(win): 1. 安装vscode 2. 安装插件Remote-ssh 离线安装 VSCode 插件的步骤如下&#xff1a; ### 1. 下载插件 在无法联网的环境中&#xff0c;首先你需要在有网络的环境下下载所需的插件。 #### 下载步骤&#xff1a; 1. 打开 [VSCode 插件市场](ht…

C++ 二分法

二分法&#xff08;Binary Search&#xff09;是一种常用的查找算法&#xff0c;它通过将已排序的元素划分为两部分&#xff0c;然后通过比较目标值与划分点的大小关系&#xff0c;将查找范围缩小一半&#xff0c;从而快速地找到目标值。二分法的时间复杂度为O(logN)&#xff0…