Skip to content
On this page

日期选择器

效果

2023年
2024年
2025年
2026年
2027年
2028年
2029年
2030年
2031年
2032年
1月
2月
3月
4月
5月
6月
7月
8月
9月
10月
11月
12月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

思路

样式

  1. 外层容器固定高度并且设置 overflow:auto;position:relative,内层容器自适应高度

  2. 中间的日期选择器部分设置 position:absolute,top:50%

  3. 遮罩层也是一个绝对定位,使用background-image:

    html
    <div 
       class="absolute 
         inset-0 
         cursor-pointer 
         pointer-events-none" 
    
       style="background-image:linear-gradient(
         180deg,
         hsla(0,0%,100%,1),
         hsla(0,0%,100%,0.8),
         hsla(0,0%,100%,0),
         hsla(0,0%,100%,0.8),
         hsla(0,0%,100%,1)
       )">
    
       </div>
  4. 确定滚动位置
    父容器使用 scroll-snap-type(scroll-snap-type CSS 属性定义在滚动容器中的一个临时点(snap point)如何被严格的执行)

    子容器使用 scroll-snap-align: center; 表示与滚动容器的对齐方式, 保证滚动到中间

联动

监听这三个的容器的滚动事件,根据滚动高度, 滚动高度 / 单个高度 计算出滚动下标
当滚动年份容器时,需要把 月份/天数 容器滚动到下标为 0 的位置
当滚动月份容器时,需要把 天数 容器滚动到下标为 0 的位置

监听滚动 年份/月份 的下标,计算对应的date个数

源码

vue
<template>
  <div class="border border-solid border-red-400 rounded-md my-4 flex justify-between">
    <div class="relative flex-1 cursor-pointer font-bold" v-for="d in data">
      <div class="overflow-y-scroll h-[250px] snap-y snap-mandatory" :ref="getScrollRef">
        <div class="test">
          <div class="h-[50px] snap-center">
          </div>
          <div class="h-[50px] snap-center">
          </div>
          <div class="flex box-border  justify-center items-center h-[50px] snap-center" v-for="(item, index) in d"
            :key="index">
            {{ item }}
          </div>
          <div class="h-[50px] snap-center">
          </div>
          <div class="h-[50px] snap-center">
          </div>
        </div>
      </div>
      <div
        class="absolute left-0 -z-10 right-0 box-border h-[50px] top-[100px] border-gray-600 border-x-0 border-solid border-y">
      </div>

      <div class="absolute inset-0   cursor-pointer pointer-events-none" style="background-image:linear-gradient(
      180deg,
      hsla(0,0%,100%,1),
      hsla(0,0%,100%,0.8),
      hsla(0,0%,100%,0),
      hsla(0,0%,100%,0.8),
      hsla(0,0%,100%,1)
    )">

      </div>
    </div>

  </div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, reactive, watch } from "vue";
const years = ref(['2023年', '2024年', '2025年', '2026年', '2027年', '2028年', '2029年', '2030年', '2031年', '2032年'])
const months = ref(['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']);
const yearIndex = ref(0)
const monthIndex = ref(0)
const dates = ref([])
const scrollRefs = ref([])

const isScrollRefIndex = ref(-1)

watch(() => [yearIndex.value, monthIndex.value], ([yearIndex, monthIndex]) => {
  const date = new Date(+years.value[yearIndex].slice(0, -1), +months.value[monthIndex].slice(0, -1), 0).getDate();
  dates.value = Array.from({ length: date }, (_, i) => i + 1)
}, {
  immediate: true
})

const data = computed(() => {
  return [years.value, months.value, dates.value]
})



const findScrollIndex = (e: HTMLElement) => {
  if (!e) return 0
  const scrollTop = e.scrollTop;
  const scrollIndex = scrollTop / 50;
  return scrollIndex
}

const getScrollRef = (el: HTMLElement) => {
  scrollRefs.value.push(el)
}

const handleScroll = () => {
  scrollRefs.value.forEach((scrollRef, index) => {
    if (isScrollRefIndex.value < index) {
      scrollRef.scrollTo({
        top: 0,
        behavior: "smooth"
      })
    }
  })

  if (isScrollRefIndex.value === 0) {
    yearIndex.value = Math.ceil(findScrollIndex(scrollRefs.value[0]))
  } else if (isScrollRefIndex.value === 1) {
    monthIndex.value = Math.ceil(findScrollIndex(scrollRefs.value[1]))
  }
}

onMounted(() => {
  scrollRefs.value.forEach((scrollRef, index) => {
    scrollRef.addEventListener("scroll", () => {
      isScrollRefIndex.value = index
      handleScroll()
    })
  })
})
</script>

<style lang="scss" scoped>
::-webkit-scrollbar {
  display: none;
}
</style>