值:2026-09-01 至 2026-09-30
<script setup lang="ts">
import { ref } from 'vue'
import { CalendarRange } from '@lucide/vue'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/vue'
const range = ref<DateRangeValue | null>({ start: '2026-09-01', end: '2026-09-30' })
</script>
<template>
<Stack gap="sm" align="start">
<DateRangeField v-model="range" clearable aria-label="活动期间" class="w-96">
<template #leading>
<CalendarRange />
</template>
</DateRangeField>
<Text tone="muted" size="sm">
值:{{ range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空' }}
</Text>
</Stack>
</template>
用法
import { DateRangeField } from '@hina-ui/vue'
日期范围输入框在一个输入面里放开始与结束两组日期段,中间以「至」分隔。每段只接受数字,输入完一段后自动进入下一段,填完开始日期后直接进入结束日期。v-model 绑定 { start, end } 对象,两个字段都是 ISO 8601 字符串,格式与 DateField 相同;只填了一侧时另一侧为 null,两侧都为空时整个值为 null。未声明的属性都会传给包裹各段的组元素,请用 aria-label 或者 aria-labelledby 命名。
值:空
<script setup lang="ts">
import { ref } from 'vue'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/vue'
const range = ref<DateRangeValue | null>(null)
</script>
<template>
<Stack gap="sm" align="start">
<DateRangeField v-model="range" aria-label="发售期间" class="w-96" />
<Text tone="muted" size="sm">
值:{{ range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空' }}
</Text>
</Stack>
</template>
示例
日期与时间
granularity 决定精度,两侧同时生效:默认 day 只有日期,hour 增加小时,minute 增加小时与分钟,second 再增加秒。hourCycle 指定 12 小时制或者 24 小时制,默认随语言决定。
值:2026-09-04T19:00 至 2026-09-04T21:30
<script setup lang="ts">
import { ref } from 'vue'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/vue'
const range = ref<DateRangeValue | null>({
start: '2026-09-04T19:00',
end: '2026-09-04T21:30',
})
</script>
<template>
<Stack gap="sm" align="start">
<DateRangeField v-model="range" granularity="minute" aria-label="直播时段" class="w-[30rem]" />
<Text tone="muted" size="sm">
值:{{ range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空' }}
</Text>
</Stack>
</template>
范围
min 与 max 限定两侧可选的范围。任一侧超出范围,或者结束日期早于开始日期时,输入框显示为校验未通过,但值仍然会更新。placeholder 指定没有值时各段从哪个日期开始增减,默认是今天。
<script setup lang="ts">
import { ref } from 'vue'
import { DateRangeField, type DateRangeValue } from '@hina-ui/vue'
const range = ref<DateRangeValue | null>({ start: '2026-09-20', end: '2026-09-10' })
</script>
<template>
<DateRangeField
v-model="range"
min="2026-09-01"
max="2026-09-30"
placeholder="2026-09-01"
aria-label="活动期间"
class="w-96"
/>
</template>
可清除
clearable 在末尾加一个清除按钮,一次清空两侧,清空后值为 null,焦点回到第一段。
<script setup lang="ts">
import { ref } from 'vue'
import { DateRangeField, type DateRangeValue } from '@hina-ui/vue'
const range = ref<DateRangeValue | null>({ start: '2026-09-01', end: '2026-09-30' })
</script>
<template>
<DateRangeField v-model="range" clearable aria-label="活动期间" class="w-96" />
</template>
尺寸
size 有 sm、md、lg 三档,与同档输入框等高。
<script setup lang="ts">
import { DateRangeField, Stack } from '@hina-ui/vue'
const range = { start: '2026-09-01', end: '2026-09-30' }
</script>
<template>
<Stack class="w-96">
<DateRangeField size="sm" :model-value="range" aria-label="小号" />
<DateRangeField size="md" :model-value="range" aria-label="中号" />
<DateRangeField size="lg" :model-value="range" aria-label="大号" />
</Stack>
</template>
状态
invalid 给输入面加上警示色,disabled 禁用整组,readonly 只读,仍然可以聚焦与选中。variant="secondary" 是放在 surface 之内的扁平形态。
<script setup lang="ts">
import { DateRangeField, Stack } from '@hina-ui/vue'
const range = { start: '2026-09-01', end: '2026-09-30' }
</script>
<template>
<Stack class="w-96">
<DateRangeField :model-value="range" invalid aria-label="校验未通过" />
<DateRangeField :model-value="range" disabled aria-label="已禁用" />
<DateRangeField :model-value="range" readonly aria-label="只读" />
<DateRangeField :model-value="range" variant="secondary" aria-label="扁平形态" />
</Stack>
</template>
在表单中
放进 FormField 后,标签通过 aria-labelledby 关联到整个区间输入,错误信息由字段渲染;校验规则与提交交给 Form。区间的值是一个对象,只填了一端时另一端是 null,校验完整性写在对象层,错误才会落到这个字段上。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, DateRangeField, Form, FormField, Text, type DateRangeValue } from '@hina-ui/vue'
const schema = v.object({
period: v.pipe(
v.nullable(v.object({ start: v.nullable(v.string()), end: v.nullable(v.string()) })),
v.check(value => !!value?.start && !!value?.end, '请填写完整的活动期间'),
),
})
const values = reactive({ period: null as DateRangeValue | null })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
<FormField name="period" label="活动期间" required>
<DateRangeField v-model="values.period" />
</FormField>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
行为
- 点击任意一段即可输入;点击输入面的空白处时,焦点落到第一个空段,开始日期填满时落到结束日期的第一段。
- 每段只接受数字,输入完一段后自动进入下一段,开始日期的最后一段填完后进入结束日期;左右方向键在段之间移动,上下方向键增减,退格清空当前段。
- 一侧的所有段都填满时才会更新该侧的值;清空任意一段后,该侧变为
null。 - 结束早于开始,或者任一侧超出
min与max时,值仍然会更新,同时标为校验未通过。
无障碍
- 各段是
role="spinbutton",名称带「开始日期」或者「结束日期」前缀,分隔文字与分隔符对辅助技术隐藏。 - 通过
aria-label或者aria-labelledby为整组命名。 invalid、结束早于开始与超出范围都会在组元素上设置aria-invalid。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | DateRangeValue | null | null | { start, end },两侧都是 ISO 8601 字符串 |
placeholder | string | 今天 | 空值时各段的起点,ISO 8601 字符串 |
min | string | — | 可选范围的下限 |
max | string | — | 可选范围的上限 |
granularity | 'day' | 'hour' | 'minute' | 'second' | 'day' | 精度 |
hourCycle | 12 | 24 | 随语言 | 小时制 |
clearable | boolean | false | 是否显示清除按钮 |
readonly | boolean | false | 是否只读 |
name | string | — | 表单字段名 |
variant | 'primary' | 'secondary' | 'primary' | 形态 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
disabled | boolean | false | 是否禁用 |
invalid | boolean | false | 是否处于校验未通过状态 |
class | string | — | 追加至根元素的类名 |
DateRangeValue 是 { start: string | null; end: string | null },可以从包入口导入。
插槽
| 插槽 | 说明 |
|---|---|
leading | 前置附属格的内容 |
trailing | 后置附属格的内容 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | value: DateRangeValue | null | 值变化 |
clear | — | 点击清除 |