能否实现带横向可见月份拨盘与日期拨盘的Date picker?
横向滚动式日期选择器实现方案
当然可以实现你描述的这种横向滚动、中间位置选中的日期选择器,下面是具体的实现思路和代码示例:
核心实现要点
- 双横向滚动容器:上层放置月份列表,下层放置对应月份的日期列表,通过同步滚动保持两者位置对应
- 中间选中判定:监听滚动事件,计算容器视口中间位置对应的元素,将其标记为选中状态
- 日期联动:切换月份时自动更新下层的日期列表,确保日期与月份匹配
- 滚动对齐优化:利用滚动吸附特性,让滚动结束后选中元素自动对齐到容器中间,避免半选中状态
Vue 快速实现示例
模板部分
<template> <div class="date-picker-container"> <!-- 月份滚动容器 --> <div ref="monthScroll" class="scroll-container" @scroll="handleScroll"> <div v-for="month in months" :key="month.value" :data-value="month.value" :class="['month-item', { active: selectedMonth === month.value }]" > {{ month.label }} </div> </div> <!-- 日期滚动容器 --> <div ref="dayScroll" class="scroll-container" @scroll="handleScroll"> <div v-for="day in currentDays" :key="day" :class="['day-item', { active: selectedDay === day }]" > {{ day }} </div> </div> <!-- 选中日期展示 --> <div class="selected-date">选中日期:{{ selectedMonthLabel }} {{ selectedDay }}日</div> </div> </template>
脚本部分
<script setup> import { ref, computed, onMounted } from 'vue' const months = ref([ { label: '1月', value: 1 }, { label: '2月', value: 2 }, { label: '3月', value: 3 }, { label: '4月', value: 4 }, { label: '5月', value: 5 }, { label: '6月', value: 6 }, { label: '7月', value: 7 }, { label: '8月', value: 8 }, { label: '9月', value: 9 }, { label: '10月', value: 10 }, { label: '11月', value: 11 }, { label: '12月', value: 12 }, ]) const monthScroll = ref(null) const dayScroll = ref(null) const selectedMonth = ref(new Date().getMonth() + 1) const selectedDay = ref(new Date().getDate()) const selectedMonthLabel = computed(() => { return months.value.find(m => m.value === selectedMonth.value)?.label || '' }) const currentDays = computed(() => { const daysInMonth = new Date(new Date().getFullYear(), selectedMonth.value, 0).getDate() return Array.from({ length: daysInMonth }, (_, i) => i + 1) }) const handleScroll = (e) => { // 处理月份选中 if (e.target === monthScroll.value) { const containerMid = monthScroll.value.scrollLeft + monthScroll.value.clientWidth / 2 const items = monthScroll.value.querySelectorAll('.month-item') items.forEach(item => { const itemMid = item.offsetLeft + item.clientWidth / 2 if (Math.abs(itemMid - containerMid) < item.clientWidth / 2) { selectedMonth.value = parseInt(item.dataset.value) // 同步日期滚动到选中日位置 setTimeout(() => { const targetDay = dayScroll.value.querySelector(`.day-item:nth-child(${selectedDay.value})`) if (targetDay) { dayScroll.value.scrollLeft = targetDay.offsetLeft - (dayScroll.value.clientWidth / 2) + (targetDay.clientWidth / 2) } }, 0) } }) } // 处理日期选中 if (e.target === dayScroll.value) { const containerMid = dayScroll.value.scrollLeft + dayScroll.value.clientWidth / 2 const items = dayScroll.value.querySelectorAll('.day-item') items.forEach(item => { const itemMid = item.offsetLeft + item.clientWidth / 2 if (Math.abs(itemMid - containerMid) < item.clientWidth / 2) { selectedDay.value = parseInt(item.textContent) // 此处可添加加载当日条目的逻辑 // loadDailyItems(selectedMonth.value, selectedDay.value) } }) } } onMounted(() => { // 初始化滚动到当前日期位置 const initMonthItem = monthScroll.value.querySelector(`.month-item:nth-child(${selectedMonth.value})`) const initDayItem = dayScroll.value.querySelector(`.day-item:nth-child(${selectedDay.value})`) if (initMonthItem) { monthScroll.value.scrollLeft = initMonthItem.offsetLeft - (monthScroll.value.clientWidth / 2) + (initMonthItem.clientWidth / 2) } if (initDayItem) { dayScroll.value.scrollLeft = initDayItem.offsetLeft - (dayScroll.value.clientWidth / 2) + (initDayItem.clientWidth / 2) } }) </script>
样式部分
<style scoped> .date-picker-container { width: 100%; max-width: 600px; margin: 20px auto; } .scroll-container { display: flex; overflow-x: auto; padding: 10px 0; scroll-snap-type: x mandatory; scrollbar-width: none; } .scroll-container::-webkit-scrollbar { display: none; } .month-item, .day-item { flex-shrink: 0; width: 60px; text-align: center; padding: 8px 0; border-radius: 4px; cursor: pointer; scroll-snap-align: center; transition: background-color 0.2s; } .month-item.active, .day-item.active { background-color: #2196F3; color: white; } .selected-date { margin-top: 20px; text-align: center; font-size: 16px; font-weight: bold; } </style>
原生 JavaScript 实现思路
- 创建两个带横向滚动的
div容器,分别承载月份和日期列表 - 动态生成月份选项和对应月份的日期选项
- 为两个容器绑定
scroll事件,计算视口中间位置对应的元素,标记为选中状态 - 实现月份切换时的日期列表更新逻辑
- 利用 CSS 的
scroll-snap属性优化滚动对齐体验
关键交互优化
- 隐藏滚动条,让界面更简洁
- 添加过渡动画,提升选中状态的视觉反馈
- 滚动结束后自动触发当日条目的加载逻辑
- 适配移动端触摸滑动操作
内容的提问来源于stack exchange,提问作者Saurabh Bisht
相关产品推荐
相关产品推荐

