Date Range Picker 日期区间

用于日期区间场景的 MoriUI 组件。

导入

import { DateRangePicker } from 'moriui'

示例

概览

选择日期区间。

2024
1
20
2024
2
10
已选择:2024年1月20日 – 2024年2月10日
<script setup lang="ts">
  import { shallowRef } from 'vue'
  import { CalendarDate } from '@internationalized/date'
  import {
    DateRangePicker,
    DateRangePickerCalendar,
    DateRangePickerCell,
    DateRangePickerCellTrigger,
    DateRangePickerContent,
    DateRangePickerField,
    DateRangePickerGrid,
    DateRangePickerGridBody,
    DateRangePickerGridHead,
    DateRangePickerGridRow,
    DateRangePickerHeadCell,
    DateRangePickerHeader,
    DateRangePickerHeading,
    DateRangePickerInput,
    DateRangePickerNext,
    DateRangePickerPrev,
    DateRangePickerTrigger
  } from 'moriui'

  const value = shallowRef({
    start: new CalendarDate(2024, 1, 20),
    end: new CalendarDate(2024, 2, 10)
  })

  function format(date: CalendarDate | undefined): string {
    if (!date)
      return ''
    return `${date.year}年${date.month}月${date.day}日`
  }
</script>

<template>
  <div class="flex flex-col items-center gap-4">
    <DateRangePicker v-model="value" locale="zh-CN" class="w-[320px]">
      <DateRangePickerField v-slot="{ segments }">
        <DateRangePickerInput
          v-for="segment in segments.start"
          :key="`start-${segment.part}`"
          type="start"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <span aria-hidden="true">–</span>
        <DateRangePickerInput
          v-for="segment in segments.end"
          :key="`end-${segment.part}`"
          type="end"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <DateRangePickerTrigger aria-label="选择日期范围" />
      </DateRangePickerField>
      <DateRangePickerContent>
        <DateRangePickerCalendar v-slot="{ grid, weekDays }">
          <DateRangePickerHeader>
            <DateRangePickerPrev aria-label="上个月" />
            <DateRangePickerHeading />
            <DateRangePickerNext aria-label="下个月" />
          </DateRangePickerHeader>
          <DateRangePickerGrid v-for="month in grid" :key="month.value.toString()">
            <DateRangePickerGridHead>
              <DateRangePickerGridRow>
                <DateRangePickerHeadCell v-for="weekDay in weekDays" :key="weekDay">
                  {{ weekDay }}
                </DateRangePickerHeadCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridHead>
            <DateRangePickerGridBody>
              <DateRangePickerGridRow v-for="(week, index) in month.rows" :key="index">
                <DateRangePickerCell v-for="day in week" :key="day.toString()" :date="day">
                  <DateRangePickerCellTrigger :day="day" :month="month.value" />
                </DateRangePickerCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridBody>
          </DateRangePickerGrid>
        </DateRangePickerCalendar>
      </DateRangePickerContent>
    </DateRangePicker>
    <div class="text-sm text-muted-foreground">
      <template v-if="value.start && value.end">
        已选择:{{ format(value.start) }} – {{ format(value.end) }}
      </template>
      <template v-else>
        请选择日期范围
      </template>
    </div>
  </div>
</template>

基础

最小化区间选择。

2024
4
1
2024
4
15
<script setup lang="ts">
  import { shallowRef } from 'vue'
  import { CalendarDate } from '@internationalized/date'
  import {
    DateRangePicker,
    DateRangePickerCalendar,
    DateRangePickerCell,
    DateRangePickerCellTrigger,
    DateRangePickerContent,
    DateRangePickerField,
    DateRangePickerGrid,
    DateRangePickerGridBody,
    DateRangePickerGridHead,
    DateRangePickerGridRow,
    DateRangePickerHeadCell,
    DateRangePickerHeader,
    DateRangePickerHeading,
    DateRangePickerInput,
    DateRangePickerNext,
    DateRangePickerPrev,
    DateRangePickerTrigger
  } from 'moriui'

  const value = shallowRef({
    start: new CalendarDate(2024, 4, 1),
    end: new CalendarDate(2024, 4, 15)
  })
</script>

<template>
  <DateRangePicker v-model="value" locale="zh-CN">
    <DateRangePickerField v-slot="{ segments }">
      <DateRangePickerInput
        v-for="segment in segments.start"
        :key="`start-${segment.part}`"
        type="start"
        :part="segment.part"
      >
        {{ segment.value }}
      </DateRangePickerInput>
      <span aria-hidden="true">–</span>
      <DateRangePickerInput
        v-for="segment in segments.end"
        :key="`end-${segment.part}`"
        type="end"
        :part="segment.part"
      >
        {{ segment.value }}
      </DateRangePickerInput>
      <DateRangePickerTrigger aria-label="选择日期范围" />
    </DateRangePickerField>
    <DateRangePickerContent>
      <DateRangePickerCalendar v-slot="{ grid, weekDays }">
        <DateRangePickerHeader>
          <DateRangePickerPrev aria-label="上个月" />
          <DateRangePickerHeading />
          <DateRangePickerNext aria-label="下个月" />
        </DateRangePickerHeader>
        <DateRangePickerGrid v-for="month in grid" :key="month.value.toString()">
          <DateRangePickerGridHead>
            <DateRangePickerGridRow>
              <DateRangePickerHeadCell v-for="day in weekDays" :key="day">
                {{ day }}
              </DateRangePickerHeadCell>
            </DateRangePickerGridRow>
          </DateRangePickerGridHead>
          <DateRangePickerGridBody>
            <DateRangePickerGridRow v-for="(week, idx) in month.rows" :key="idx">
              <DateRangePickerCell v-for="day in week" :key="day.toString()" :date="day">
                <DateRangePickerCellTrigger :day="day" :month="month.value" />
              </DateRangePickerCell>
            </DateRangePickerGridRow>
          </DateRangePickerGridBody>
        </DateRangePickerGrid>
      </DateRangePickerCalendar>
    </DateRangePickerContent>
  </DateRangePicker>
</template>

预设

快捷预设日期范围。

2026
7
16
2026
7
22
已选择:2026年7月16日 – 2026年7月22日
<script setup lang="ts">
  import { shallowRef } from 'vue'
  import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date'
  import {
    DateRangePicker,
    DateRangePickerCalendar,
    DateRangePickerCell,
    DateRangePickerCellTrigger,
    DateRangePickerContent,
    DateRangePickerField,
    DateRangePickerGrid,
    DateRangePickerGridBody,
    DateRangePickerGridHead,
    DateRangePickerGridRow,
    DateRangePickerHeadCell,
    DateRangePickerHeader,
    DateRangePickerHeading,
    DateRangePickerInput,
    DateRangePickerNext,
    DateRangePickerPrev,
    DateRangePickerTrigger
  } from 'moriui'

  const value = shallowRef({
    start: today(getLocalTimeZone()).subtract({ days: 6 }),
    end: today(getLocalTimeZone())
  })

  const presets = [
    {
      label: '最近 7 天',
      get: () => {
        const end = today(getLocalTimeZone())
        const start = end.subtract({ days: 6 })
        return { start, end }
      }
    },
    {
      label: '本月',
      get: () => {
        const now = today(getLocalTimeZone())
        const start = new CalendarDate(now.year, now.month, 1)
        const end = start.add({ months: 1 }).subtract({ days: 1 })
        return { start, end }
      }
    },
    {
      label: '上个月',
      get: () => {
        const now = today(getLocalTimeZone())
        const start = new CalendarDate(now.year, now.month, 1).subtract({ months: 1 })
        const end = new CalendarDate(now.year, now.month, 1).subtract({ days: 1 })
        return { start, end }
      }
    },
    {
      label: '最近 30 天',
      get: () => {
        const end = today(getLocalTimeZone())
        const start = end.subtract({ days: 29 })
        return { start, end }
      }
    },
    {
      label: '最近 90 天',
      get: () => {
        const end = today(getLocalTimeZone())
        const start = end.subtract({ days: 89 })
        return { start, end }
      }
    },
    {
      label: '今年',
      get: () => {
        const now = today(getLocalTimeZone())
        const start = new CalendarDate(now.year, 1, 1)
        const end = new CalendarDate(now.year, 12, 31)
        return { start, end }
      }
    }
  ]

  function applyPreset(preset: typeof presets[number]) {
    value.value = preset.get()
  }

  function format(date: CalendarDate | undefined): string {
    if (!date)
      return ''
    return `${date.year}年${date.month}月${date.day}日`
  }
</script>

<template>
  <div class="flex flex-col items-center gap-4">
    <DateRangePicker v-model="value" locale="zh-CN">
      <DateRangePickerField v-slot="{ segments }">
        <DateRangePickerInput
          v-for="segment in segments.start"
          :key="`start-${segment.part}`"
          type="start"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <span aria-hidden="true">–</span>
        <DateRangePickerInput
          v-for="segment in segments.end"
          :key="`end-${segment.part}`"
          type="end"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <DateRangePickerTrigger aria-label="选择日期范围" />
      </DateRangePickerField>
      <DateRangePickerContent>
        <div class="flex flex-wrap gap-1 p-2 border-b border-border">
          <button
            v-for="preset in presets"
            :key="preset.label"
            type="button"
            class="text-xs px-2 py-1 rounded hover:bg-accent hover:text-accent-foreground transition-colors"
            @click="applyPreset(preset)"
          >
            {{ preset.label }}
          </button>
        </div>
        <DateRangePickerCalendar v-slot="{ grid, weekDays }">
          <DateRangePickerHeader>
            <DateRangePickerPrev aria-label="上个月" />
            <DateRangePickerHeading />
            <DateRangePickerNext aria-label="下个月" />
          </DateRangePickerHeader>
          <DateRangePickerGrid v-for="month in grid" :key="month.value.toString()">
            <DateRangePickerGridHead>
              <DateRangePickerGridRow>
                <DateRangePickerHeadCell v-for="weekDay in weekDays" :key="weekDay">
                  {{ weekDay }}
                </DateRangePickerHeadCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridHead>
            <DateRangePickerGridBody>
              <DateRangePickerGridRow v-for="(week, index) in month.rows" :key="index">
                <DateRangePickerCell v-for="day in week" :key="day.toString()" :date="day">
                  <DateRangePickerCellTrigger :day="day" :month="month.value" />
                </DateRangePickerCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridBody>
          </DateRangePickerGrid>
        </DateRangePickerCalendar>
      </DateRangePickerContent>
    </DateRangePicker>
    <div v-if="value.start && value.end" class="text-sm text-muted-foreground">
      已选择:{{ format(value.start) }} – {{ format(value.end) }}
    </div>
  </div>
</template>

RTL

从右到左的日期区间选择。

20
1
2024
10
2
2024
<script setup lang="ts">
  import { shallowRef } from 'vue'
  import { CalendarDate } from '@internationalized/date'
  import {
    DateRangePicker,
    DateRangePickerCalendar,
    DateRangePickerCell,
    DateRangePickerCellTrigger,
    DateRangePickerContent,
    DateRangePickerField,
    DateRangePickerGrid,
    DateRangePickerGridBody,
    DateRangePickerGridHead,
    DateRangePickerGridRow,
    DateRangePickerHeadCell,
    DateRangePickerHeader,
    DateRangePickerHeading,
    DateRangePickerInput,
    DateRangePickerNext,
    DateRangePickerPrev,
    DateRangePickerTrigger,
    DirectionProvider
  } from 'moriui'

  const value = shallowRef({
    start: new CalendarDate(2024, 1, 20),
    end: new CalendarDate(2024, 2, 10)
  })
</script>

<template>
  <DirectionProvider dir="rtl">
    <DateRangePicker v-model="value" locale="ar">
      <DateRangePickerField v-slot="{ segments }">
        <DateRangePickerInput
          v-for="segment in segments.start"
          :key="`start-${segment.part}`"
          type="start"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <span aria-hidden="true">–</span>
        <DateRangePickerInput
          v-for="segment in segments.end"
          :key="`end-${segment.part}`"
          type="end"
          :part="segment.part"
        >
          {{ segment.value }}
        </DateRangePickerInput>
        <DateRangePickerTrigger aria-label="اختيار نطاق التاريخ" />
      </DateRangePickerField>
      <DateRangePickerContent>
        <DateRangePickerCalendar v-slot="{ grid, weekDays }">
          <DateRangePickerHeader>
            <DateRangePickerPrev aria-label="الشهر السابق" />
            <DateRangePickerHeading />
            <DateRangePickerNext aria-label="الشهر التالي" />
          </DateRangePickerHeader>
          <DateRangePickerGrid v-for="month in grid" :key="month.value.toString()">
            <DateRangePickerGridHead>
              <DateRangePickerGridRow>
                <DateRangePickerHeadCell v-for="weekDay in weekDays" :key="weekDay">
                  {{ weekDay }}
                </DateRangePickerHeadCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridHead>
            <DateRangePickerGridBody>
              <DateRangePickerGridRow v-for="(week, index) in month.rows" :key="index">
                <DateRangePickerCell v-for="day in week" :key="day.toString()" :date="day">
                  <DateRangePickerCellTrigger :day="day" :month="month.value" />
                </DateRangePickerCell>
              </DateRangePickerGridRow>
            </DateRangePickerGridBody>
          </DateRangePickerGrid>
        </DateRangePickerCalendar>
      </DateRangePickerContent>
    </DateRangePicker>
  </DirectionProvider>
</template>

使用说明

组合方式

DateRangePicker 为日期区间选择的包装组件,提供起止日期双输入。

RTL

方向由 DirectionProvider 控制。

变体

DateRangePicker 没有视觉变体;弹层定位由 Popper 控制。

可访问性

DateRangePicker 将 Calendar range 模式与双输入字段组合。输入应有各自的 Label 或 aria-label。

API 参考

属性类型默认值说明
DateRangePicker v-model{ start: DateValue | null, end: DateValue | null }undefined起止日期对象。
DateRangePicker placeholderDateValueundefined无选择时的占位月份。