效果如下图:

在线预览
APIs
受控 / 非受控:传入 v-model:value 即受控,组件只经 update:value 上报;不传时由组件自持,可用 defaultValue 指定初值。两者同时传时以 value 为准。
DatePicker Props
名称说明类型默认值
| value v-model |
双向绑定值,毫秒时间戳;范围形态为两段元组,某段可为 null;传入即受控,不传时组件自持(可用 defaultValue 指定初值,缺省无选中值) |
number | [number | null, number | null] | null |
undefined |
| defaultValue |
非受控初始值,未绑 v-model:value 时生效;与面板初始日期 defaultPickerValue、默认时分秒 defaultTime 不同,本字段决定选中值本身 |
number | [number | null, number | null] | null |
undefined |
| open v-model |
面板是否展开;传入即受控,内部触发只经 update:open / openChange 上报(不传时由组件内部维护,缺省收起) |
boolean |
undefined |
| formattedValue v-model |
字符串轨道的双向绑定值,传入时以它为准(受控);显示格式由 format / valueFormat 决定 |
string | [string, string] | null |
undefined |
| type |
选择形态 |
DatePickerType |
‘date’ |
| format |
日期展示格式,未传时按 DefaultFormat,参考 format |
string |
undefined |
| valueFormat |
绑定值格式,默认与 format 相同 |
string |
undefined |
| placeholder |
输入框提示文字,范围形态可传入两段文案 |
string | [string, string] |
‘’ |
| defaultPickerValue |
面板初始日期(毫秒时间戳),默认取 value 或今天 |
number |
undefined |
| startDayOfWeek |
一周的开始是星期几,0-6,0 是周一;周形态恒按 ISO 周(周一起始),该属性对周形态不生效 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
0 |
| disabledDate |
不可选择的日期,入参为「当日零点」的毫秒时间戳;月 / 季 / 年形态下按整段判定(整段都不可选该格才禁用) |
(timestamp: number) => boolean |
undefined |
| disabledTime |
不可选择的时间,仅带时间的形态生效;入参为当前面板草稿值的时间戳,范围形态额外接收段标识 |
DatePickerDisabledTime | DatePickerRangeDisabledTime |
undefined |
| defaultTime |
选中日期时的默认时分秒(只取其中的时分秒,不参与时间面板的预选展示);范围形态可按段传入两段元组,某段为 undefined 时该段退回当前时刻 |
number | [number | undefined, number | undefined] |
undefined |
| timePickerProps |
时间面板选项(步长 / 12 小时制 / 隐藏禁用项) |
DatePickerTimeProps |
undefined |
| presets |
预设选项,点击即提交并收起 |
DatePickerPreset[] |
[] |
| width |
选择器宽度,单位 px,不传时随内容自适应 |
string | number |
undefined |
| size |
选择器大小 |
‘small’ | ‘middle’ | ‘large’ |
‘middle’ |
| status |
校验状态 |
‘warning’ | ‘error’ |
undefined |
| bordered |
是否展示边框 |
boolean |
true |
| disabled |
是否禁用;范围形态可传两段元组逐段禁用(被禁用的段不可输入,且该段日期被对端的值锁死) |
boolean | [boolean, boolean] |
false |
| allowClear |
是否展示清除按钮 |
boolean |
true |
| allowEmpty |
范围形态各段是否允许为空,为空时该段可被单独清空并对外提交 |
[boolean, boolean] |
[false, false] |
| inputReadOnly |
输入框是否只读(避免移动端唤起键盘) |
boolean |
false |
| to |
面板挂载的容器节点:显式传入时按此挂载(元素标签名 (例如 'body') 或元素本身,false 会待在原地);不传时优先挂到最近的承载层内容容器(Modal / Drawer / Dialog 卡片或上层浮层面板),无承载层时为 body |
string | HTMLElement | false |
undefined |
| placement |
面板弹出位置 |
‘topLeft’ | ‘top’ | ‘topRight’ | ‘bottomLeft’ | ‘bottom’ | ‘bottomRight’ |
‘bottomLeft’ |
| showArrow |
范围形态的面板指示箭头是否展示:不传时跟随弹出方位(仅左侧对齐的 bottomLeft / topLeft 展示),true 始终展示、false 始终隐藏 |
boolean |
undefined |
| showToday |
是否展示面板底部的「今天」快捷,面板切到周 / 月 / 季 / 年视图时隐藏 |
boolean |
true |
| showNow |
是否展示面板底部的「此刻」快捷,仅带时间的形态生效 |
boolean |
true |
| dateRender |
自定义日期单元格的内容与样式(替换日号),单元格容器与选中 / 悬浮 / 区间底色仍由组件提供;入参 current 为该格日期的零点时间戳、today 为当前时刻时间戳 |
(params: { current: number; today: number }) => VNode | VNode[] |
undefined |
| renderExtraFooter |
面板底部的额外页脚,渲染在「今天」「确定」等操作行之上 |
() => VNode | VNode[] |
undefined |
| suffixIcon |
自定义选择框后缀图标 |
VNode | (() => VNode) |
undefined |
| panelClass |
面板额外类名 |
string |
‘’ |
| panelStyle |
面板额外样式 |
CSSProperties |
undefined |
| zIndex |
面板层级,优先级最高(覆盖默认层级与 ConfigProvider 的 baseZIndex 自动分配) |
number |
undefined |
DatePickerType Type
名称值
| DatePickerType |
‘date’ | ‘datetime’ | ‘week’ | ‘month’ | ‘quarter’ | ‘year’ | ‘daterange’ | ‘datetimerange’ | ‘weekrange’ | ‘monthrange’ | ‘quarterrange’ | ‘yearrange’ |
DatePickerDisabledTime Type
不可选择的时间判定,入参为当前面板草稿值的时间戳。
名称值
| DatePickerDisabledTime |
(timestamp: number) => DatePickerDisabledTimeUnits |
DatePickerRangeDisabledTime Type
范围形态的不可选择时间判定,side 标识当前作用在起点还是终点。
名称值
| DatePickerRangeDisabledTime |
(timestamp: number, side: ‘start’ | ‘end’) => DatePickerDisabledTimeUnits |
DatePickerDisabledTimeUnits Type
名称说明类型
| disabledHours? |
禁用的「时」 |
() => number[] |
| disabledMinutes? |
禁用的「分」,入参为选中的「时」 |
(hour: number) => number[] |
| disabledSeconds? |
禁用的「秒」,入参为选中的「时」「分」 |
(hour: number, minute: number) => number[] |
DatePickerTimeProps Type
名称说明类型
| hourStep? |
「时」的步长,默认 1;须能整除 24,非法值按 1 处理 |
number |
| minuteStep? |
「分」的步长,默认 1;须能整除 60,非法值按 1 处理 |
number |
| secondStep? |
「秒」的步长,默认 1;须能整除 60,非法值按 1 处理 |
number |
| use12Hours? |
是否使用 12 小时制,与 12 小时制格式(hh)配合使用 |
boolean |
| hideDisabledOptions? |
是否隐藏禁用的选项 |
boolean |
DatePickerPreset Type
名称说明类型
| label |
预设名称 |
string | VNode |
| value |
预设值:单选形态为一个时间戳,范围形态为两段元组;可为返回预设值的函数(点击时才求值) |
number | [number, number] | (() => number | [number, number]) |
DatePickerRangeInfo Type
范围形态草稿变更的来源信息。
名称说明类型
| range |
草稿变更落在哪一段 |
‘start’ | ‘end’ |
DefaultFormat Value
名称值
| date |
‘yyyy-MM-dd’ |
| datetime |
‘yyyy-MM-dd HH:mm:ss’ |
| week |
‘RRRR-II周’ |
| month |
‘yyyy-MM’ |
| quarter |
‘yyyy-QQQ’ |
| year |
‘yyyy’ |
| daterange |
‘yyyy-MM-dd’ |
| datetimerange |
‘yyyy-MM-dd HH:mm:ss’ |
| weekrange |
‘RRRR-II周’ |
| monthrange |
‘yyyy-MM’ |
| quarterrange |
‘yyyy-QQQ’ |
| yearrange |
‘yyyy’ |
format 支持的格式化占位符列表
format / valueFormat 采用 date-fns 占位符,常用占位符:
标识示例描述
| yy |
26 |
年,两位数 |
| yyyy |
2026 |
年,四位数 |
| M |
1-12 |
月 |
| MM |
01-12 |
月,两位数 |
| Q |
1-4 |
季度 |
| QQQ |
Q1-Q4 |
季度,带 Q 前缀 |
| d |
1-31 |
日 |
| dd |
01-31 |
日,两位数 |
| h |
1-12 |
小时,12 小时制 |
| hh |
01-12 |
小时,12 小时制,两位数 |
| H |
0-23 |
小时 |
| HH |
00-23 |
小时,两位数 |
| m |
0-59 |
分钟 |
| mm |
00-59 |
分钟,两位数 |
| s |
0-59 |
秒 |
| ss |
00-59 |
秒,两位数 |
| ww |
01-53 |
第几周,两位数(本地周口径) |
| II |
01-53 |
ISO 周序号,两位数 |
| RRRR |
2026 |
ISO 周年,四位数 |
周形态的默认格式为 RRRR-II周(如 2026-41周):「周」是字面量后缀,中文语境下用于指明该数字为周序号,不需要时传入 format="RRRR-II" 即可移除;date-fns 不允许「周年 + 本地周序号」同串(yyyy 与 ww 会解析失败),故周序号须与周年同为 ISO 口径。
Slots
名称说明用法
| dateRender |
自定义日期单元格的内容与样式(替换日号),优先于同名属性 |
v-slot:dateRender |
| renderExtraFooter |
面板底部的额外页脚,优先于同名属性 |
v-slot:renderExtraFooter |
| suffixIcon |
自定义选择框后缀图标 |
v-slot:suffixIcon |
| separator |
范围形态两段之间的分隔符 |
v-slot:separator |
Events
名称说明类型
| change |
值变化时的回调 |
(value: number | [number | null, number | null] | null, formattedValue: string | [string, string] | null) => void |
| calendarChange |
范围形态待选区间变化时的回调,info.range 标识本次改动落在哪一段 |
(value: [number | null, number | null] | null, formattedValue: [string, string] | null, info: DatePickerRangeInfo) => void |
| ok |
带时间的形态点击「确定」时的回调 |
(value: number | [number | null, number | null] | null, formattedValue: string | [string, string] | null) => void |
| openChange |
面板展开收起时的回调 |
(open: boolean) => void |
| panelChange |
面板视图切换时的回调 |
(value: number, mode: string) => void |
| focus |
输入框获取焦点时的回调,relatedTarget 为失去焦点的元素 |
(e: FocusEvent) => void |
| blur |
输入框失焦的回调,relatedTarget 为获得焦点的元素 |
(e: FocusEvent) => void |
Methods
名称说明类型
| focus |
获取选择器焦点 |
() => void |
| blur |
使选择器失去焦点 |
() => void |
创建日期选择器组件 DatePicker.vue
<script setup lang="ts">
import { computed, nextTick, ref, useSlots, watch } from 'vue'
import type { CSSProperties, TransitionProps, VNode } from 'vue'
import Popup from 'components/popup'
import {
DatePanel,
DatetimePanel,
PickerIcon,
PickerTrigger,
PresetPanel,
RangePanel,
formatTimestamp,
getDefaultFormat,
getDefaultTime,
getInputSize,
getPanelModeOf,
isOutOfRangeBoundary,
isRangeType,
PANEL_NAV_KEYS,
parseTimestamp,
type PickerActiveSection,
type PickerDateRender,
type PickerDateRenderParams,
type PickerDefaultTime,
type PickerDisabledTime,
type PickerFormattedValue,
type PickerPanelMode,
type PickerPreset,
type PickerPresetValue,
type PickerRangeDisabledTime,
type PickerRangeFormattedValue,
type PickerRangeSide,
type PickerRangeValue,
type PickerSelectSource,
type PickerSize,
type PickerStatus,
type PickerTimePanelProps,
type PickerType,
type PickerValue,
type StartDayOfWeek
} from 'components/picker'
import type { FloatingPlacement, MountElement } from 'components/utils'
import { FLOATING_LAYER_Z_INDEX, useContainerClose, useControlledModel, useSlotsExist } from 'components/utils'
export interface Props {
value?: PickerValue
defaultValue?: PickerValue
open?: boolean
formattedValue?: PickerFormattedValue
type?: PickerType
format?: string
valueFormat?: string
placeholder?: string | [string, string]
defaultPickerValue?: number
startDayOfWeek?: StartDayOfWeek
disabledDate?: (timestamp: number) => boolean
disabledTime?: PickerDisabledTime | PickerRangeDisabledTime
defaultTime?: PickerDefaultTime
timePickerProps?: PickerTimePanelProps
presets?: PickerPreset[]
width?: string | number
size?: PickerSize
status?: PickerStatus
bordered?: boolean
disabled?: boolean | [boolean, boolean]
allowClear?: boolean
allowEmpty?: [boolean, boolean]
inputReadOnly?: boolean
to?: string | MountElement | false
placement?: 'topLeft' | 'top' | 'topRight' | 'bottomLeft' | 'bottom' | 'bottomRight'
showArrow?: boolean
showToday?: boolean
showNow?: boolean
dateRender?: (params: PickerDateRenderParams) => VNode | VNode[]
renderExtraFooter?: () => VNode | VNode[]
suffixIcon?: VNode | (() => VNode)
panelClass?: string
panelStyle?: CSSProperties
zIndex?: number
}
export interface DatePickerRangeInfo {
range: PickerRangeSide
}
export interface DatePickerSlots {
suffixIcon?: () => VNode[]
separator?: () => VNode[]
dateRender?: (params: PickerDateRenderParams) => VNode[]
renderExtraFooter?: () => VNode[]
}
export type {
PickerValue as DatePickerValue,
PickerFormattedValue as DatePickerFormattedValue,
PickerType as DatePickerType,
PickerDisabledTime as DatePickerDisabledTime,
PickerRangeDisabledTime as DatePickerRangeDisabledTime,
PickerDisabledTimeUnits as DatePickerDisabledTimeUnits,
PickerTimePanelProps as DatePickerTimeProps,
PickerPreset as DatePickerPreset
} from 'components/picker'
const props = withDefaults(defineProps<Props>(), {
value: undefined,
defaultValue: undefined,
open: undefined,
formattedValue: undefined,
type: 'date',
format: undefined,
valueFormat: undefined,
placeholder: '',
defaultPickerValue: undefined,
startDayOfWeek: 0,
disabledDate: undefined,
disabledTime: undefined,
defaultTime: undefined,
timePickerProps: undefined,
presets: () => [],
width: undefined,
size: 'middle',
status: undefined,
bordered: true,
disabled: false,
allowClear: true,
allowEmpty: () => [false, false],
inputReadOnly: false,
to: undefined,
placement: 'bottomLeft',
showArrow: undefined,
showToday: true,
showNow: true,
dateRender: undefined,
renderExtraFooter: undefined,
suffixIcon: undefined,
panelClass: '',
panelStyle: undefined,
zIndex: undefined
})
defineSlots<DatePickerSlots>()
const emits = defineEmits<{
change: [value: PickerValue, formattedValue: PickerFormattedValue]
calendarChange: [
value: PickerRangeValue | null,
formattedValue: PickerRangeFormattedValue | null,
info: DatePickerRangeInfo
]
ok: [value: PickerValue, formattedValue: PickerFormattedValue]
'update:formattedValue': [value: PickerFormattedValue]
'update:value': [value: PickerValue]
'update:open': [open: boolean]
openChange: [open: boolean]
panelChange: [value: number, mode: PickerPanelMode]
focus: [event: FocusEvent]
blur: [event: FocusEvent]
}>()
const valueModel = useControlledModel<PickerValue>('value', props.defaultValue ?? null)
const openModel = useControlledModel<boolean>('open', false)
const triggerRef = ref<InstanceType<typeof PickerTrigger> | null>(null)
const popupRef = ref<InstanceType<typeof Popup> | null>(null)
const wrapperRef = ref<HTMLElement | null>(null)
const POPUP_OFFSET = 4
const RANGE_ARROW_SIZE = 16
const RANGE_POPUP_OFFSET = POPUP_OFFSET + (RANGE_ARROW_SIZE * 2) / 3
const ARROW_PLACEMENTS: FloatingPlacement[] = ['bottomLeft', 'topLeft']
const themeVars = computed(() => ({
'–picker-primary-color': 'var(–va-cp-date-picker-primary-color, var(–va-primary-color))',
'–picker-primary-color-hover': 'var(–va-cp-date-picker-primary-color-hover, var(–va-primary-color-hover))',
'–picker-primary-shadow-color': 'var(–va-cp-date-picker-primary-bg-color, var(–va-primary-bg-color))',
'–picker-primary-color-bg': 'var(–va-cp-date-picker-primary-bg-color, var(–va-primary-bg-color))',
'–picker-time-column-active-bg': 'var(–va-cp-date-picker-primary-bg-color, var(–va-primary-bg-color))',
'–picker-primary-color-bg-hover': 'var(–va-cp-date-picker-primary-bg-color, var(–va-primary-bg-color))',
'–picker-hover-border-color': 'var(–va-cp-date-picker-primary-border-color, var(–va-primary-border-color))'
}))
const wrapperStyle = computed(() => {
const width =
props.width === undefined ? undefined : typeof props.width === 'number' ? `${props.width}px` : props.width
return { …themeVars.value, '–datepicker-width': width ?? 'auto' } as CSSProperties
})
const mergedPanelClass = computed(() => ['datepicker-panel-container', props.panelClass].filter(Boolean).join(' '))
const slots: DatePickerSlots = useSlots()
const slotsExist = useSlotsExist(['dateRender', 'renderExtraFooter'])
const mergedDateRender = computed<PickerDateRender | undefined>(() => {
if (slotsExist.dateRender) {
return (params) => slots.dateRender?.(params) ?? []
}
const render = props.dateRender
if (!render) {
return undefined
}
return (params) => {
const node = render(params)
return Array.isArray(node) ? node : [node]
}
})
const mergedExtraFooter = computed<(() => VNode[]) | undefined>(() => {
if (slotsExist.renderExtraFooter) {
return () => slots.renderExtraFooter?.() ?? []
}
const render = props.renderExtraFooter
if (!render) {
return undefined
}
return () => {
const node = render()
return Array.isArray(node) ? node : [node]
}
})
const isDateTime = computed(() => props.type === 'datetime')
const isRange = computed(() => isRangeType(props.type))
const disabledSides = computed<[boolean, boolean]>(() => {
const { disabled } = props
if (Array.isArray(disabled)) {
return isRange.value ? [disabled[0], disabled[1]] : [disabled[0] || disabled[1], disabled[0] || disabled[1]]
}
return [disabled, disabled]
})
const fullyDisabled = computed(() => disabledSides.value[0] && disabledSides.value[1])
const isDateTimeRange = computed(() => props.type === 'datetimerange')
const panelBaseMode = computed<PickerPanelMode>(() => getPanelModeOf(props.type))
const needConfirm = computed(() => isDateTime.value || isDateTimeRange.value)
const popupOffset = computed(() => (isRange.value ? RANGE_POPUP_OFFSET : POPUP_OFFSET))
const mergedFormat = computed(() => props.format ?? getDefaultFormat(props.type))
const mergedValueFormat = computed(() => props.valueFormat ?? mergedFormat.value)
const inputSize = computed(() => getInputSize(mergedValueFormat.value))
const formattedControlled = computed(() => props.formattedValue !== undefined)
const innerValue = computed<number | null>(() => {
if (formattedControlled.value) {
return typeof props.formattedValue === 'string'
? parseTimestamp(props.formattedValue, mergedValueFormat.value)
: null
}
return typeof valueModel.value === 'number' ? valueModel.value : null
})
function parseSideTimestamp(text: string | null | undefined): number | null {
return parseTimestamp(text ?? '', mergedValueFormat.value)
}
function formatSideTimestamp(timestamp: number | null | undefined): string {
return timestamp === null || timestamp === undefined ? '' : formatTimestamp(timestamp, mergedValueFormat.value)
}
const rangeValue = computed<PickerRangeValue | null>(() => {
if (formattedControlled.value) {
const formatted = props.formattedValue
if (!Array.isArray(formatted)) {
return null
}
return [parseSideTimestamp(formatted[0]), parseSideTimestamp(formatted[1])]
}
const raw = valueModel.value
if (!Array.isArray(raw)) {
return null
}
return [raw[0], raw[1]]
})
const draftValue = ref<number | null>(null)
const draftRange = ref<PickerRangeValue | null>(null)
const activeSide = ref<PickerRangeSide>('start')
const activeTriggerIndex = computed(() => (activeSide.value === 'start' ? 0 : 1))
const hoverTimestamp = ref<number | null>(null)
const presetPreview = ref<PickerRangeValue | null>(null)
const hoverPreviewIndex = computed<0 | 1 | null>(() => {
const hovered = hoverTimestamp.value
const isRangeType = isRange.value
const isPanelOpen = openModel.value
if (!isRangeType || !isPanelOpen || hovered === null) {
return null
}
return activeTriggerIndex.value
})
const singlePreviewTimestamp = computed<number | null>(() => {
const hovered = hoverTimestamp.value
const isRangeType = isRange.value
const isPanelOpen = openModel.value
return !isRangeType && isPanelOpen ? hovered : null
})
const rangeArrowLeft = ref(0)
const activatedSides = ref<Record<PickerRangeSide, boolean>>({ start: false, end: false })
function activateSide(side: PickerRangeSide) {
activeSide.value = side
activatedSides.value[side] = true
}
function resetActivatedSides() {
activatedSides.value = { start: false, end: false }
}
const panelTimestamp = computed(() => {
const drafting = openModel.value && draftValue.value !== null
return drafting ? draftValue.value : innerValue.value
})
const panelRangeValue = computed<PickerRangeValue | null>(() => {
const drafting = isRange.value && openModel.value && draftRange.value !== null
return drafting ? draftRange.value : rangeValue.value
})
const mergedRangeDisabledDate = computed<((timestamp: number) => boolean) | undefined>(() => {
const userDisabledDate = props.disabledDate
if (!isRange.value) {
return userDisabledDate
}
const current = panelRangeValue.value
const isStartSide = activeSide.value === 'start'
const boundary = (isStartSide ? current?.[1] : current?.[0]) ?? null
const otherSideActivated = isStartSide ? activatedSides.value.end : activatedSides.value.start
const otherSideDisabled = isStartSide ? disabledSides.value[1] : disabledSides.value[0]
if (boundary === null || (!otherSideActivated && !otherSideDisabled)) {
return userDisabledDate
}
return (timestamp: number) => {
if (userDisabledDate?.(timestamp)) {
return true
}
return isOutOfRangeBoundary(timestamp, boundary, !isStartSide)
}
})
const singleDisabledTime = computed<PickerDisabledTime | undefined>(() => {
const disabledTime = props.disabledTime
return disabledTime ? (timestamp) => (disabledTime as PickerDisabledTime)(timestamp) : undefined
})
const rangeDisabledTime = computed<PickerRangeDisabledTime | undefined>(() => {
const disabledTime = props.disabledTime
return isDateTimeRange.value && disabledTime ? (disabledTime as PickerRangeDisabledTime) : undefined
})
const singleDefaultTime = computed(() => getDefaultTime(props.defaultTime))
const displayText = computed(() => {
if (formattedControlled.value) {
return typeof props.formattedValue === 'string' ? props.formattedValue : ''
}
const preview = singlePreviewTimestamp.value
if (preview !== null) {
return formatTimestamp(preview, mergedValueFormat.value)
}
return panelTimestamp.value === null ? '' : formatTimestamp(panelTimestamp.value, mergedValueFormat.value)
})
const displayRangeTexts = computed<[string, string]>(() => {
const formatted = props.formattedValue
let texts: [string, string]
if (formattedControlled.value && Array.isArray(formatted)) {
texts = [formatted[0] ?? '', formatted[1] ?? '']
} else {
const current = panelRangeValue.value
texts = [formatSideTimestamp(current?.[0]), formatSideTimestamp(current?.[1])]
}
const hovered = hoverTimestamp.value
const isRangeType = isRange.value
const isPanelOpen = openModel.value
if (isRangeType && isPanelOpen && hovered !== null) {
texts[activeTriggerIndex.value] = formatSideTimestamp(hovered)
}
return texts
})
const mergedPlaceholders = computed<[string, string]>(() => {
const placeholder = props.placeholder
if (Array.isArray(placeholder)) {
return [placeholder[0] ?? '', placeholder[1] ?? '']
}
return [placeholder, placeholder]
})
const singlePlaceholder = computed(() => (Array.isArray(props.placeholder) ? '' : props.placeholder))
const showClear = computed(() => {
if (!props.allowClear || fullyDisabled.value) {
return false
}
if (isRange.value) {
return (rangeValue.value?.[0] ?? null) !== null || (rangeValue.value?.[1] ?? null) !== null
}
return innerValue.value !== null
})
const actualPlacement = computed<FloatingPlacement>(() => popupRef.value?.actualPlacement ?? props.placement)
const showRangeArrow = computed(() => {
if (!isRange.value || props.showArrow === false) return false
return props.showArrow === true || ARROW_PLACEMENTS.includes(actualPlacement.value)
})
const panelTransitionProps: TransitionProps = { name: 'datepicker-zoom' }
watch(
() => openModel.value,
(next) => {
if (next) {
draftRange.value = rangeValue.value
activateSide(activeSide.value)
return
}
draftValue.value = null
draftRange.value = null
resetActivatedSides()
hoverTimestamp.value = null
presetPreview.value = null
},
{ immediate: true }
)
useContainerClose(() => requestOpen(false))
function requestOpen(next: boolean) {
if (fullyDisabled.value || openModel.value === next) return
openModel.value = next
emits('openChange', next)
}
function onTriggerClick(index: 0 | 1 = 0) {
if (keyboardPanelMode.value) {
keyboardPanelMode.value = false
activeSection.value = null
}
if (openModel.value) return
if (disabledSides.value[index]) return
activateSide(index === 0 ? 'start' : 'end')
requestOpen(true)
nextTick(() => {
triggerRef.value?.focus(index)
})
}
function commit(timestamp: number | null): PickerFormattedValue {
const nextFormatted = timestamp === null ? null : formatTimestamp(timestamp, mergedValueFormat.value)
valueModel.value = timestamp
emits('update:formattedValue', nextFormatted)
emits('change', timestamp, nextFormatted)
return nextFormatted
}
function onSelect(timestamp: number, source: PickerSelectSource = 'mouse') {
if (source === 'key') {
draftValue.value = timestamp
return
}
commit(timestamp)
requestOpen(false)
}
function formatRangeValue(range: PickerRangeValue | null): PickerRangeFormattedValue | null {
if (range === null) {
return null
}
return [formatSideTimestamp(range[0]), formatSideTimestamp(range[1])]
}
function commitRange(range: PickerRangeValue | null): PickerRangeFormattedValue | null {
const nextFormatted = formatRangeValue(range)
valueModel.value = range
emits('update:formattedValue', nextFormatted)
emits('change', range, nextFormatted)
return nextFormatted
}
function canTriggerSide(timestamp: number | null, index: 0 | 1): boolean {
return timestamp !== null || (props.allowEmpty[index] ?? false)
}
function applyRangeSide(timestamp: number, side: PickerRangeSide, switchSide: boolean) {
const current = draftRange.value ?? rangeValue.value ?? [null, null]
let next: PickerRangeValue = side === 'start' ? [timestamp, current[1]] : [current[0], timestamp]
if (next[0] !== null && next[1] !== null && next[0] > next[1]) {
next = side === 'start' ? [timestamp, null] : [null, timestamp]
}
const nextFormatted = formatRangeValue(next)
const canCommit = canTriggerSide(next[0], 0) && canTriggerSide(next[1], 1)
draftRange.value = next
emits('calendarChange', next, nextFormatted, { range: side })
if (canCommit) {
commitRange(next)
}
const otherSide: PickerRangeSide = side === 'start' ? 'end' : 'start'
const currentIndex = side === 'start' ? 0 : 1
const otherIndex = side === 'start' ? 1 : 0
const otherSideDisabled = disabledSides.value[otherIndex]
const keepOpen =
switchSide &&
!otherSideDisabled &&
next[currentIndex] !== null &&
(!activatedSides.value[otherSide] || next[otherIndex] === null)
if (keepOpen) {
activateSide(otherSide)
nextTick(() => triggerRef.value?.focus(otherIndex))
return { value: next, formatted: nextFormatted }
}
requestOpen(false)
return { value: next, formatted: nextFormatted }
}
function onRangeSelect(timestamp: number, side: PickerRangeSide, source: PickerSelectSource = 'mouse') {
if (source === 'key') {
const current = draftRange.value ?? rangeValue.value ?? [null, null]
draftRange.value = side === 'start' ? [timestamp, current[1]] : [current[0], timestamp]
return
}
applyRangeSide(timestamp, side, true)
}
function onRangeDraftChange(timestamp: number, side: PickerRangeSide) {
const current = draftRange.value ?? rangeValue.value ?? [null, null]
draftRange.value = side === 'start' ? [timestamp, current[1]] : [current[0], timestamp]
}
function onRangeConfirm(timestamp: number, side: PickerRangeSide) {
const { value: range, formatted } = applyRangeSide(timestamp, side, true)
emits('ok', range, formatted)
}
function onRangeHover(timestamp: number | null) {
hoverTimestamp.value = timestamp
}
function onSingleHover(timestamp: number | null) {
hoverTimestamp.value = timestamp
}
function onSideFocus(index: 0 | 1) {
activateSide(index === 0 ? 'start' : 'end')
}
function onDraftChange(timestamp: number) {
draftValue.value = timestamp
}
function onConfirm(timestamp: number, source: 'ok' | 'now') {
const nextFormatted = commit(timestamp)
requestOpen(false)
if (source === 'ok') {
emits('ok', timestamp, nextFormatted)
}
}
function onClear() {
if (isRange.value) {
commitRange(null)
} else {
commit(null)
}
requestOpen(false)
}
function onPresetSelect(presetValue: PickerPresetValue) {
if (isRange.value) {
if (!Array.isArray(presetValue)) return
commitRange([presetValue[0], presetValue[1]])
} else {
if (Array.isArray(presetValue)) return
commit(presetValue)
}
requestOpen(false)
}
function onPresetHover(presetValue: PickerPresetValue | null) {
if (presetValue === null || !Array.isArray(presetValue)) {
presetPreview.value = null
return
}
presetPreview.value = [presetValue[0], presetValue[1]]
}
function onTextConfirm(text: string, source: 'enter' | 'blur', index: 0 | 1) {
if (source === 'blur' && (needConfirm.value || keyboardPanelMode.value)) {
draftValue.value = null
draftRange.value = null
return
}
if (isRange.value) {
const rangeTimestamp = parseTimestamp(text, mergedValueFormat.value)
if (rangeTimestamp !== null) {
applyRangeSide(rangeTimestamp, index === 0 ? 'start' : 'end', source === 'enter')
}
return
}
const timestamp = parseTimestamp(text, mergedValueFormat.value)
if (timestamp === null) return
commit(timestamp)
requestOpen(false)
}
function onEscKeydown() {
if (openModel.value) {
requestOpen(false)
}
}
function onPanelChange(timestamp: number, mode: PickerPanelMode) {
hoverTimestamp.value = null
emits('panelChange', timestamp, mode)
}
interface PanelKeydownTarget {
handleKeydown: (event: KeyboardEvent) => boolean
}
const keyboardPanelMode = ref(false)
const activeSection = ref<PickerActiveSection>(null)
const hasTimeSection = computed(() => isDateTime.value || isDateTimeRange.value)
const PANEL_OPEN_IGNORED_KEYS = ['Shift', 'Control', 'Alt', 'Meta', 'Escape', 'Enter']
const rangePanelRef = ref<PanelKeydownTarget | null>(null)
const datetimePanelRef = ref<PanelKeydownTarget | null>(null)
const datePanelRef = ref<PanelKeydownTarget | null>(null)
watch(openModel, () => {
keyboardPanelMode.value = false
activeSection.value = null
})
watch(activeSide, () => {
activeSection.value = null
})
function getRenderedPanel(): PanelKeydownTarget | null {
if (isRange.value) {
return rangePanelRef.value
}
return isDateTime.value ? datetimePanelRef.value : datePanelRef.value
}
function onTriggerInput(): void {
keyboardPanelMode.value = false
activeSection.value = null
}
function onTriggerFocus(event: FocusEvent): void {
onTriggerInput()
emits('focus', event)
}
function forwardKeydown(event: KeyboardEvent): boolean {
if (hasTimeSection.value && activeSection.value === null) {
activeSection.value = 'date'
event.preventDefault()
return true
}
return getRenderedPanel()?.handleKeydown(event) ?? false
}
const SECTION_CYCLE: PickerActiveSection[] = [null, 'date', 'time']
function getNextSection(current: PickerActiveSection, backward: boolean): PickerActiveSection {
const index = SECTION_CYCLE.indexOf(current)
return SECTION_CYCLE[backward ? index – 1 : index + 1] ?? null
}
function onTabKeydown(event: KeyboardEvent): void {
if (!keyboardPanelMode.value) {
keyboardPanelMode.value = true
event.preventDefault()
return
}
if (!hasTimeSection.value) {
return
}
const nextSection = getNextSection(activeSection.value, event.shiftKey)
activeSection.value = nextSection
if (nextSection !== null) {
event.preventDefault()
}
}
function onWrapperKeydown(event: KeyboardEvent): void {
if (event.key === 'Tab') {
if (openModel.value && !fullyDisabled.value) {
onTabKeydown(event)
}
return
}
if (!openModel.value) {
if (!PANEL_OPEN_IGNORED_KEYS.includes(event.key)) {
requestOpen(true)
}
return
}
if (!keyboardPanelMode.value || !PANEL_NAV_KEYS.includes(event.key)) {
return
}
forwardKeydown(event)
}
function focus() {
triggerRef.value?.focus()
}
function blur() {
triggerRef.value?.blur()
}
defineExpose({ focus, blur })
</script>
<template>
<div
ref="wrapperRef"
class="datepicker-wrap"
:style="wrapperStyle"
@keydown="onWrapperKeydown"
@keydown.esc="onEscKeydown"
@keydown.enter.stop
@input="onTriggerInput"
>
<PickerTrigger
ref="triggerRef"
:text="displayText"
:texts="displayRangeTexts"
:range="isRange"
:active–index="activeTriggerIndex"
:preview–index="hoverPreviewIndex"
:preview="singlePreviewTimestamp !== null"
:placeholder="singlePlaceholder"
:placeholders="mergedPlaceholders"
:size="size"
:status="status"
:bordered="bordered"
:disabled="fullyDisabled"
:disabled–sides="disabledSides"
:input–read–only="inputReadOnly || keyboardPanelMode"
:allow–clear="allowClear && showClear"
:open="openModel"
:input–size="inputSize"
:suffix–icon="suffixIcon"
@click="onTriggerClick"
@clear="onClear"
@arrow–change="rangeArrowLeft = $event"
@side–focus="onSideFocus"
@text–confirm="onTextConfirm"
@focus="onTriggerFocus"
@blur="emits('blur', $event)"
>
<template #suffix>
<slot name="suffixIcon">
<component :is="suffixIcon" v–if="suffixIcon" />
</slot>
</template>
<template #separator>
<slot name="separator">
<PickerIcon name="swap-right" />
</slot>
</template>
</PickerTrigger>
<Popup
ref="popupRef"
:show="openModel && !fullyDisabled"
:to="to"
:anchor="wrapperRef"
:placement="placement"
:offset="popupOffset"
:panel–class="mergedPanelClass"
:panel–style="{ …panelStyle, …themeVars }"
:z–index="zIndex"
:default–z–index="FLOATING_LAYER_Z_INDEX.select"
:transition–props="panelTransitionProps"
close–on–outside–click
@keydown.esc="onEscKeydown"
@outside–click="requestOpen(false)"
>
<RangePanel
v–if="isRange"
ref="rangePanelRef"
:value="panelRangeValue"
:base–mode="panelBaseMode"
:active="openModel"
:active–section="activeSection"
:active–side="activeSide"
:start–day–of–week="startDayOfWeek"
:disabled–date="mergedRangeDisabledDate"
:date–render="mergedDateRender"
:default–picker–value="defaultPickerValue"
:extra–footer="mergedExtraFooter"
:datetime="isDateTimeRange"
:format="mergedFormat"
:header–format="format"
:time–props="timePickerProps"
:disabled–time="rangeDisabledTime"
:default–time="defaultTime"
:preview–value="presetPreview"
@select="onRangeSelect"
@draft–change="onRangeDraftChange"
@confirm="onRangeConfirm"
@panel–change="onPanelChange"
@hover–change="onRangeHover"
>
<template #presets>
<PresetPanel v–if="presets.length" :presets="presets" @select="onPresetSelect" @hover="onPresetHover" />
</template>
</RangePanel>
<!— 范围形态的面板指示箭头:与激活段下划线同源平移、随弹出方位上下翻转,展示时机由 showArrow 与方位共同决定 —>
<div v–if="showRangeArrow" class="datepicker-range-arrow" :style="{ left: `${rangeArrowLeft}px` }" />
<DatetimePanel
v–else–if="isDateTime"
ref="datetimePanelRef"
:value="panelTimestamp"
:active–section="activeSection"
:format="mergedFormat"
:header–format="format"
:time–props="timePickerProps"
:disabled–date="disabledDate"
:disabled–time="singleDisabledTime"
:date–render="mergedDateRender"
:default–time="singleDefaultTime"
:default–picker–value="defaultPickerValue"
:extra–footer="mergedExtraFooter"
:start–day–of–week="startDayOfWeek"
:show–now="showNow"
:active="openModel"
@change="onDraftChange"
@confirm="onConfirm"
@panel–change="onPanelChange"
@cell–hover="onSingleHover($event)"
@cell–leave="onSingleHover(null)"
>
<template #presets>
<PresetPanel v–if="presets.length" :presets="presets" @select="onPresetSelect" @hover="onPresetHover" />
</template>
</DatetimePanel>
<DatePanel
v–else
ref="datePanelRef"
:value="panelTimestamp"
:base–mode="panelBaseMode"
:active="openModel"
:start–day–of–week="startDayOfWeek"
:disabled–date="disabledDate"
:date–render="mergedDateRender"
:default–picker–value="defaultPickerValue"
:extra–footer="mergedExtraFooter"
:show–today="showToday"
@select="onSelect"
@panel–change="onPanelChange"
@cell–hover="onSingleHover($event)"
@cell–leave="onSingleHover(null)"
>
<template #presets>
<PresetPanel v–if="presets.length" :presets="presets" @select="onPresetSelect" @hover="onPresetHover" />
</template>
</DatePanel>
</Popup>
</div>
</template>
<style lang="less" scoped>
.datepicker–wrap {
display: inline–flex;
width: var(—datepicker–width, auto);
:deep(.picker–trigger) {
flex: auto;
min–width: 0;
}
}
.datepicker–range–arrow {
position: absolute;
top: 0;
z–index: 1;
width: 16px;
height: 16px;
margin–left: 16.5px;
overflow: hidden;
pointer–events: none;
transform: translateY(–100%);
transition: left 0.3s ease–out;
&::before {
position: absolute;
bottom: 0;
left: 0;
width: 16px;
height: 8px;
background–color: var(—picker–panel–bg–color, var(—va–bg–elevated–color));
clip–path: polygon(
1.6568542494923806px 100%,
50% 1.6568542494923806px,
14.34314575050762px 100%,
1.6568542494923806px 100%
);
clip–path: path(
'M 0 8 A 4 4 0 0 0 2.82842712474619 6.82842712474619 L 6.585786437626905 3.0710678118654755 A 2 2 0 0 1 9.414213562373096 3.0710678118654755 L 13.17157287525381 6.82842712474619 A 4 4 0 0 0 16 8 Z'
);
content: '';
}
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
width: 8.970562748477143px;
height: 8.970562748477143px;
margin: auto;
z–index: –1;
background: transparent;
border–radius: 0 0 2px 0;
box–shadow: 3px 3px 7px rgba(0, 0, 0, 0.1);
transform: translateY(50%) rotate(–135deg);
content: '';
}
}
</style>
<style lang="less">
.datepicker–panel–container {
width: max–content;
outline: none;
}
.va–popup–placement–top .datepicker–range–arrow,
.va–popup–placement–topLeft .datepicker–range–arrow,
.va–popup–placement–topRight .datepicker–range–arrow {
top: auto;
bottom: 0;
transform: translateY(100%) rotate(180deg);
}
.datepicker–panel–container.va–popup–leaving .picker–panel–body .picker–panel–cell–inner {
transition: none;
}
.datepicker–zoom–enter–active,
.datepicker–zoom–leave–active {
animation–duration: 0.2s;
animation–fill–mode: both;
}
.datepicker–zoom–enter–active {
animation–name: datepickerZoomIn;
animation–timing–function: cubic–bezier(0.23, 1, 0.32, 1);
}
.datepicker–zoom–leave–active {
animation–name: datepickerZoomOut;
animation–timing–function: cubic–bezier(0.755, 0.05, 0.855, 0.06);
}
.datepicker–zoom–enter–from,
.datepicker–zoom–leave–to {
opacity: 0;
}
@keyframes datepickerZoomIn {
0% {
opacity: 0;
transform: scaleY(0.8);
}
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes datepickerZoomOut {
0% {
opacity: 1;
transform: scaleY(1);
}
100% {
opacity: 0;
transform: scaleY(0.8);
}
}
</style>
在要使用的页面引入
<script setup lang="ts">
import DatePicker from './DatePicker.vue'
import { computed, ref } from 'vue'
import type { CSSProperties } from 'vue'
import {
addDays,
addMonths,
differenceInCalendarDays,
endOfDay,
getDate,
getDay,
getHours,
getMinutes,
getSeconds,
isSameDay,
startOfDay,
startOfMonth
} from 'date-fns'
import type { DatePickerProps } from 'vue-amazing-ui'
const fixedDate = new Date(2026, 9, 10).getTime()
function initialRange(startOffset: number = 0, endOffset: number = 7): [number, number] {
const start = startOfDay(addDays(new Date(), startOffset)).getTime()
const end = startOfDay(addDays(new Date(), endOffset)).getTime()
return [start, end]
}
const basicValue = ref<number | null>(null)
const slashValue = ref<number | null>(fixedDate)
const chineseValue = ref<number | null>(fixedDate)
const rangeFormatValue = ref<[number, number] | null>([fixedDate, addDays(fixedDate, 5).getTime()])
const disabledValue = ref<number | null>(fixedDate)
const beforeTodayValue = ref<number | null>(null)
const afterTodayValue = ref<number | null>(null)
const weekendValue = ref<number | null>(null)
const sizeCaseValue = ref<number | null>(null)
const suffixValue = ref<number | null>(null)
const errorValue = ref<number | null>(null)
const warningValue = ref<number | null>(null)
const errorRangeValue = ref<[number, number] | null>(null)
const warningRangeValue = ref<[number, number] | null>(null)
const borderlessValue = ref<number | null>(null)
const placementBottomLeftValue = ref<number | null>(null)
const placementBottomRightValue = ref<number | null>(null)
const placementTopLeftValue = ref<number | null>(null)
const placementTopRightValue = ref<number | null>(null)
const placementRangeBottomLeftValue = ref<[number, number] | null>(null)
const placementRangeTopLeftValue = ref<[number, number] | null>(null)
const keyboardValue = ref<number | null>(null)
const keyboardRangeValue = ref<[number, number] | null>(null)
const keyboardDateTimeValue = ref<number | null>(null)
const weekValue = ref<number | null>(null)
const monthValue = ref<number | null>(null)
const quarterValue = ref<number | null>(null)
const yearValue = ref<number | null>(null)
const monthFormatValue = ref<number | null>(startOfMonth(fixedDate).getTime())
const weekFormatValue = ref<number | null>(Date.now())
const disabledMonthValue = ref<number | null>(startOfMonth(fixedDate).getTime())
const disabledFullRangeValue = ref<[number, number] | null>([fixedDate, fixedDate])
const disabledMonthBeforeValue = ref<number | null>(null)
const sizeMonthValue = ref<number | null>(null)
const sizeWeekValue = ref<number | null>(null)
const monthRangeValue = ref<[number, number] | null>(null)
const quarterRangeValue = ref<[number, number] | null>(null)
const yearRangeValue = ref<[number, number] | null>(null)
const weekRangeValue = ref<[number, number] | null>(null)
const presetMonthRangeValue = ref<[number, number] | null>(null)
const borderlessMonthValue = ref<number | null>(null)
const borderlessQuarterValue = ref<number | null>(null)
const borderlessYearValue = ref<number | null>(null)
const borderlessMonthRangeValue = ref<[number, number] | null>(null)
const borderlessYearRangeValue = ref<[number, number] | null>(null)
const borderlessWeekValue = ref<number | null>(null)
const borderlessWeekRangeValue = ref<[number, number] | null>(null)
const borderlessDateRangeValue = ref<[number, number] | null>(null)
const borderlessQuarterRangeValue = ref<[number, number] | null>(null)
const monthRangePresets: NonNullable<DatePickerProps['presets']> = [
{
label: '近 3 个月',
value: () => [startOfMonth(addMonths(new Date(), –2)).getTime(), startOfMonth(new Date()).getTime()]
},
{
label: '近半年',
value: () => [startOfMonth(addMonths(new Date(), –5)).getTime(), startOfMonth(new Date()).getTime()]
}
]
const rangeValue = ref<[number, number] | null>(null)
const sizeRangeValue = ref<[number, number] | null>(null)
const rangePlaceholder: [string, string] = ['开始日期', '结束日期']
const weekRangePlaceholder: [string, string] = ['开始周', '结束周']
const monthRangePlaceholder: [string, string] = ['开始月份', '结束月份']
const quarterRangePlaceholder: [string, string] = ['开始季度', '结束季度']
const yearRangePlaceholder: [string, string] = ['开始年份', '结束年份']
type SwitchableType = 'date' | 'week' | 'month' | 'quarter' | 'year'
const switchableType = ref<SwitchableType>('date')
const switchableValue = ref<number | null>(null)
const switchableOptions: { label: string; value: SwitchableType }[] = [
{ label: '日期', value: 'date' },
{ label: '周', value: 'week' },
{ label: '月份', value: 'month' },
{ label: '季度', value: 'quarter' },
{ label: '年份', value: 'year' }
]
const switchablePlaceholder = computed(() => {
const placeholderMap: Record<SwitchableType, string> = {
date: '请选择日期',
week: '请选择周',
month: '请选择月份',
quarter: '请选择季度',
year: '请选择年份'
}
return placeholderMap[switchableType.value]
})
const dateTimeRangeValue = ref<[number, number] | null>(null)
const defaultTimeRangeValue = ref<[number, number] | null>(null)
const rangeDefaultTime: NonNullable<DatePickerProps['defaultTime']> = [
startOfDay(new Date()).getTime(),
endOfDay(new Date()).getTime()
]
const disabledRangeValue = ref<[number, number] | null>(null)
const disabledSideRangeInit = initialRange(–3, 0)
const disabledSideRangeValue = ref<[number, number] | null>(disabledSideRangeInit)
const datetimeValue = ref<number | null>(null)
const minuteStepValue = ref<number | null>(null)
const twelveHourValue = ref<number | null>(null)
const showNowValue = ref<number | null>(null)
const eventValue = ref<number | null>(null)
const eventRangeValue = ref<[number, number] | null>(null)
const eventDateTimeValue = ref<number | null>(null)
function onEventChange(
value: number | [number | null, number | null] | null,
formattedValue: string | [string, string] | null
): void {
console.log('change', value, formattedValue)
}
function onEventCalendarChange(
value: [number | null, number | null] | null,
formattedValue: [string, string] | null,
info: { range: 'start' | 'end' }
): void {
console.log('calendarChange', value, formattedValue, info)
}
function onEventOk(
value: number | [number | null, number | null] | null,
formattedValue: string | [string, string] | null
): void {
console.log('ok', value, formattedValue)
}
function onEventOpenChange(open: boolean): void {
console.log('openChange', open)
}
function onEventPanelChange(value: number, mode: string): void {
console.log('panelChange', value, mode)
}
function onEventFocus(event: FocusEvent): void {
console.log('focus', event)
}
function onEventBlur(event: FocusEvent): void {
console.log('blur', event)
}
const disabledDateTimeValue = ref<number | null>(null)
const hideDisabledOptionsValue = ref<number | null>(null)
function disabledDateBefore(timestamp: number): boolean {
return startOfDay(timestamp).getTime() > startOfDay(new Date()).getTime()
}
function disabledDateAfter(timestamp: number): boolean {
const current = endOfDay(timestamp).getTime()
return current <= endOfDay(new Date()).getTime() || current > endOfDay(addDays(new Date(), 7)).getTime()
}
function disabledWeekendDate(timestamp: number): boolean {
const day = getDay(timestamp)
return day === 0 || day === 6
}
function disabledMonthBefore(timestamp: number): boolean {
return endOfDay(timestamp).getTime() < startOfDay(new Date()).getTime()
}
function range(start: number, end: number): number[] {
return Array.from({ length: end – start }, (_, index) => index + start)
}
function disabledDateTodayOrBefore(timestamp: number): boolean {
return endOfDay(timestamp).getTime() <= endOfDay(new Date()).getTime()
}
function disabledDateTimeUnits() {
return {
disabledHours: () => range(4, 24),
disabledMinutes: () => range(30, 60),
disabledSeconds: () => [55, 56]
}
}
const sevenDaysValue = ref<[number, number] | null>(null)
const sevenDaysDraft = ref<[number | null, number | null] | null>(null)
function disabledSevenDaysDate(timestamp: number): boolean {
const draft = sevenDaysDraft.value
if (!draft || (draft[0] === null && draft[1] === null)) {
return false
}
const tooLate = draft[0] === null ? false : differenceInCalendarDays(timestamp, draft[0]) > 7
const tooEarly = draft[1] === null ? false : differenceInCalendarDays(draft[1], timestamp) > 7
return tooLate || tooEarly
}
function onSevenDaysCalendarChange(value: [number | null, number | null] | null): void {
sevenDaysDraft.value = value
}
function onSevenDaysOpenChange(open: boolean): void {
if (open) {
sevenDaysDraft.value = null
}
}
const presetValue = ref<number | null>(null)
const presetRangeValue = ref<[number, number] | null>(null)
const presetDateTimeRangeValue = ref<[number, number] | null>(null)
function recentDaysRange(days: number): [number, number] {
return [startOfDay(addDays(new Date(), –days)).getTime(), Date.now()]
}
const datePresets: NonNullable<DatePickerProps['presets']> = [
{ label: '昨天', value: startOfDay(addDays(new Date(), –1)).getTime() },
{ label: '一周前', value: () => startOfDay(addDays(new Date(), –7)).getTime() },
{ label: '一个月前', value: () => startOfDay(addMonths(new Date(), –1)).getTime() }
]
const rangePresets: NonNullable<DatePickerProps['presets']> = [
{ label: '近 7 天', value: () => recentDaysRange(7) },
{ label: '近 14 天', value: () => recentDaysRange(14) },
{ label: '近 30 天', value: () => recentDaysRange(30) },
{ label: '近 90 天', value: () => recentDaysRange(90) }
]
const dateRenderValue = ref<number | null>(null)
const dateRenderRangeValue = ref<[number, number] | null>(null)
function firstDayCellStyle(dayOfMonth: number): CSSProperties {
const cellStyle: CSSProperties = { boxSizing: 'border-box', height: '24px' }
return dayOfMonth === 1 ? { …cellStyle, border: '1px solid #1677ff', borderRadius: '50%' } : cellStyle
}
const startValue = ref<number | null>(null)
const endValue = ref<number | null>(null)
const endOpen = ref(false)
const defaultTime = startOfDay(new Date()).getTime()
const sizeValue = ref<DatePickerProps['size']>('middle')
const sizeOptions: Array<{ label: string; value: NonNullable<DatePickerProps['size']> }> = [
{ label: 'small', value: 'small' },
{ label: 'middle', value: 'middle' },
{ label: 'large', value: 'large' }
]
function disabledStartDate(timestamp: number): boolean {
const end = endValue.value
return end !== null && differenceInCalendarDays(timestamp, end) > 0
}
function disabledEndDate(timestamp: number): boolean {
const start = startValue.value
return start !== null && differenceInCalendarDays(timestamp, start) < 0
}
interface DisabledTimeUnits {
disabledHours: () => number[]
disabledMinutes: (currentHour: number) => number[]
disabledSeconds: (currentHour: number, currentMinute: number) => number[]
}
function disabledTimeUnitsByBoundary(
hour: number,
minute: number,
second: number,
direction: 'notLaterThan' | 'notEarlierThan'
): DisabledTimeUnits {
if (direction === 'notLaterThan') {
return {
disabledHours: () => range(0, hour),
disabledMinutes: (currentHour: number) => (currentHour === hour ? range(0, minute) : []),
disabledSeconds: (currentHour: number, currentMinute: number) =>
currentHour === hour && currentMinute === minute ? range(0, second + 1) : []
}
}
return {
disabledHours: () => range(hour + 1, 24),
disabledMinutes: (currentHour: number) => (currentHour === hour ? range(minute + 1, 60) : []),
disabledSeconds: (currentHour: number, currentMinute: number) =>
currentHour === hour && currentMinute === minute ? range(second + 1, 60) : []
}
}
function disabledUnitsNotLaterThan(boundary: number, timestamp: number) {
if (!isSameDay(timestamp, boundary)) {
return {}
}
return disabledTimeUnitsByBoundary(getHours(boundary), getMinutes(boundary), getSeconds(boundary), 'notLaterThan')
}
function disabledUnitsNotEarlierThan(boundary: number, timestamp: number) {
if (!isSameDay(timestamp, boundary)) {
return {}
}
return disabledTimeUnitsByBoundary(getHours(boundary), getMinutes(boundary), getSeconds(boundary), 'notEarlierThan')
}
function disabledEndTime(timestamp: number) {
const start = startValue.value
return start === null ? {} : disabledUnitsNotLaterThan(start, timestamp)
}
function disabledStartTime(timestamp: number) {
const end = endValue.value
return end === null ? {} : disabledUnitsNotEarlierThan(end, timestamp)
}
function disabledDateTimeRangeTime(timestamp: number, side: 'start' | 'end') {
const boundary = (side === 'end' ? dateTimeRangeValue.value?.[0] : dateTimeRangeValue.value?.[1]) ?? null
if (boundary === null) {
return {}
}
return side === 'end'
? disabledUnitsNotLaterThan(boundary, timestamp)
: disabledUnitsNotEarlierThan(boundary, timestamp)
}
function onStartOpenChange(open: boolean): void {
if (!open) {
endOpen.value = true
}
}
</script>
<template>
<div>
<h1>{{ $route.name }} {{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<Space vertical>
<DatePicker v–model:value="basicValue" placeholder="请选择日期" />
<DatePicker v–model:value="weekValue" type="week" placeholder="请选择周" />
<DatePicker v–model:value="monthValue" type="month" placeholder="请选择月份" />
<DatePicker v–model:value="quarterValue" type="quarter" placeholder="请选择季度" />
<DatePicker v–model:value="yearValue" type="year" placeholder="请选择年份" />
</Space>
<h2 class="mt30 mb10">日期格式</h2>
<p class="mb10">使用 <code>format</code> 定制展示格式</p>
<Space vertical>
<DatePicker v–model:value="slashValue" format="yyyy/MM/dd" placeholder="请选择日期" />
<DatePicker v–model:value="chineseValue" format="yyyy年MM月dd日" placeholder="请选择日期" />
<DatePicker v–model:value="monthFormatValue" type="month" format="yyyy年MM月" placeholder="请选择月份" />
<!— 周形态的周年占位符取 ISO 口径(RRRR),与周序号(II)配对才可解析 —>
<DatePicker v–model:value="weekFormatValue" type="week" format="RRRR年第II周" placeholder="请选择周" />
<DatePicker
v–model:value="rangeFormatValue"
type="daterange"
format="yyyy/MM/dd"
:placeholder="rangePlaceholder"
/>
</Space>
<h2 class="mt30 mb10">范围选择器</h2>
<p class="mb10">通过设置 <code>type</code> 属性,指定范围选择器类型</p>
<Space vertical>
<DatePicker v–model:value="rangeValue" type="daterange" :placeholder="rangePlaceholder" />
<DatePicker v–model:value="weekRangeValue" type="weekrange" :placeholder="weekRangePlaceholder" />
<DatePicker v–model:value="monthRangeValue" type="monthrange" :placeholder="monthRangePlaceholder" />
<DatePicker v–model:value="quarterRangeValue" type="quarterrange" :placeholder="quarterRangePlaceholder" />
<DatePicker v–model:value="yearRangeValue" type="yearrange" :placeholder="yearRangePlaceholder" />
</Space>
<p class="mt30 mb10">
<code>type="datetimerange"</code>
在范围形态上叠加时间面板:两段共用同一个日期时间面板,点「确定」提交当前段并自动切到另一端,两段都确定后才收起;展开期间的选择只落在草稿上,关闭面板则丢弃草稿;配合
<code>disabledTime</code> 可让终点只选「晚于起点」的时分秒(日期整日越界由内核按激活段自动禁用)
</p>
<DatePicker
v–model:value="dateTimeRangeValue"
type="datetimerange"
:disabled–time="disabledDateTimeRangeTime"
:placeholder="rangePlaceholder"
/>
<h2 class="mt30 mb10">切换不同的选择器</h2>
<p class="mb10">
提供选择器,自由切换不同类型的日期选择器,常用于日期筛选场合;时间选择由 <code>TimePicker</code>
组件承担,故分段项不含「时间」项
</p>
<Space vertical>
<Segmented v–model:value="switchableType" :options="switchableOptions" />
<DatePicker v–model:value="switchableValue" :type="switchableType" :placeholder="switchablePlaceholder" />
</Space>
<h2 class="mt30 mb10">日期时间选择</h2>
<p class="mb10">
<code>type="datetime"</code> 增加选择时间功能,时间面板选项经 <code>timePickerProps</code>
透传;展开期间的选择只落在草稿值上,点「确定」或「此刻」才提交,关闭面板则丢弃草稿
</p>
<DatePicker v–model:value="datetimeValue" type="datetime" placeholder="请选择日期时间" />
<p class="mt30 mb10">分钟按 <code>5</code> 分钟步长选择</p>
<DatePicker
v–model:value="minuteStepValue"
type="datetime"
:time–picker–props="{ minuteStep: 5 }"
placeholder="请选择日期时间"
/>
<p class="mt30 mb10"
><code>12</code> 小时制:<code>use12Hours</code> 与 <code>12</code> 小时制格式(<code>hh</code>)配合使用</p
>
<DatePicker
v–model:value="twelveHourValue"
type="datetime"
format="yyyy-MM-dd hh:mm:ss"
:time–picker–props="{ use12Hours: true }"
placeholder="请选择日期时间"
/>
<p class="mt30 mb10">隐藏「此刻」快捷</p>
<DatePicker v–model:value="showNowValue" type="datetime" :show–now="false" placeholder="请选择日期时间" />
<h2 class="mt30 mb10">事件</h2>
<p class="mb10">
单选形态绑定
<code>change</code
>、<code>openChange</code>、<code>panelChange</code>、<code>focus</code>、<code>blur</code>,范围形态绑定
<code>change</code>、<code>calendarChange</code>,带时间的形态还可绑定
<code>ok</code>;各事件的参数都会打印到控制台
</p>
<Space vertical>
<DatePicker
v–model:value="eventValue"
placeholder="请选择日期"
@change="onEventChange"
@open–change="onEventOpenChange"
@panel–change="onEventPanelChange"
@focus="onEventFocus"
@blur="onEventBlur"
/>
<DatePicker
v–model:value="eventRangeValue"
type="daterange"
:placeholder="rangePlaceholder"
@change="onEventChange"
@calendar–change="onEventCalendarChange"
@open–change="onEventOpenChange"
@panel–change="onEventPanelChange"
/>
<DatePicker
v–model:value="eventDateTimeValue"
type="datetime"
placeholder="请选择日期时间"
@change="onEventChange"
@ok="onEventOk"
@open–change="onEventOpenChange"
/>
</Space>
<h2 class="mt30 mb10">禁用</h2>
<Space vertical>
<DatePicker disabled v–model:value="disabledValue" placeholder="请选择日期" />
<DatePicker disabled v–model:value="disabledMonthValue" type="month" placeholder="请选择月份" />
<DatePicker disabled v–model:value="disabledFullRangeValue" type="daterange" :placeholder="rangePlaceholder" />
</Space>
<p class="mt30 mb10">
范围形态的 <code>disabled</code> 可传两段元组逐段禁用,被禁用的段不可输入、面板也不整体置灰
</p>
<DatePicker
v–model:value="disabledSideRangeValue"
type="daterange"
:disabled="[true, false]"
:placeholder="rangePlaceholder"
/>
<h2 class="mt30 mb10">不可选择日期和时间</h2>
<p class="mb10">
<code>disabledDate</code> 与 <code>disabledTime</code> 分别禁止选择部分日期与时间,<code>disabledTime</code>
需与带时间的形态配合使用
</p>
<DatePicker
v–model:value="disabledDateTimeValue"
type="datetime"
:disabled–date="disabledDateTodayOrBefore"
:disabled–time="disabledDateTimeUnits"
:default–time="defaultTime"
placeholder="请选择日期时间"
/>
<p class="mt30 mb10">不可选择今天之后的日期</p>
<DatePicker v–model:value="beforeTodayValue" :disabled–date="disabledDateBefore" placeholder="请选择日期" />
<p class="mt30 mb10">只能选择未来七天内的日期</p>
<DatePicker v–model:value="afterTodayValue" :disabled–date="disabledDateAfter" placeholder="请选择日期" />
<p class="mt30 mb10">日期范围不可选择今天及之前(<code>disabledDate</code> 同时作用于两个面板)</p>
<DatePicker
v–model:value="disabledRangeValue"
type="daterange"
:disabled–date="disabledDateTodayOrBefore"
:placeholder="rangePlaceholder"
/>
<p class="mt30 mb10">不可选择周六与周日</p>
<DatePicker v–model:value="weekendValue" :disabled–date="disabledWeekendDate" placeholder="请选择日期" />
<p class="mt30 mb10">
月形态不可选择今天之前的月份(<code>disabledDate</code> 的粒度为整月,整月都不可选该格才禁用)
</p>
<DatePicker
v–model:value="disabledMonthBeforeValue"
type="month"
:disabled–date="disabledMonthBefore"
placeholder="请选择月份"
/>
<p class="mt30 mb10">
用 <code>time–picker–props</code> 的 <code>hideDisabledOptions</code>
把禁用的时分秒从面板中隐藏(而非置灰),适合禁用项过多的场景
</p>
<DatePicker
v–model:value="hideDisabledOptionsValue"
type="datetime"
:disabled–date="disabledDateTodayOrBefore"
:disabled–time="disabledDateTimeUnits"
:time–picker–props="{ hideDisabledOptions: true }"
placeholder="请选择日期时间"
/>
<h2 class="mt30 mb10">选择不超过七天的范围</h2>
<p class="mb10">用 <code>calendarChange</code> 记录草稿区间、用 <code>disabledDate</code> 读草稿收窄可选范围</p>
<DatePicker
v–model:value="sevenDaysValue"
type="daterange"
:disabled–date="disabledSevenDaysDate"
:placeholder="rangePlaceholder"
@calendar–change="onSevenDaysCalendarChange"
@open–change="onSevenDaysOpenChange"
/>
<h2 class="mt30 mb10">预设范围</h2>
<p class="mb10">
用 <code>presets</code> 预设常用日期与区间,点击即填入并收起;值可为时间戳,也可为返回时间戳的函数
</p>
<Space vertical>
<DatePicker v–model:value="presetValue" :presets="datePresets" placeholder="请选择日期" />
<DatePicker
v–model:value="presetRangeValue"
type="daterange"
:presets="rangePresets"
:placeholder="rangePlaceholder"
/>
<DatePicker
v–model:value="presetMonthRangeValue"
type="monthrange"
:presets="monthRangePresets"
:placeholder="monthRangePlaceholder"
/>
<DatePicker
v–model:value="presetDateTimeRangeValue"
type="datetimerange"
format="yyyy/MM/dd HH:mm:ss"
width="400px"
:presets="rangePresets"
:placeholder="rangePlaceholder"
/>
</Space>
<h2 class="mt30 mb10">额外的页脚</h2>
<p class="mb10">用 <code>renderExtraFooter</code> 在面板底部追加额外内容,渲染在「今天」「确定」等操作行之上</p>
<Space vertical>
<DatePicker placeholder="请选择日期">
<template #renderExtraFooter>
<span>extra footer</span>
</template>
</DatePicker>
<DatePicker type="datetime" placeholder="请选择日期时间">
<template #renderExtraFooter>
<span>extra footer</span>
</template>
</DatePicker>
<DatePicker type="daterange" :placeholder="rangePlaceholder">
<template #renderExtraFooter>
<span>extra footer</span>
</template>
</DatePicker>
<DatePicker type="datetimerange" :placeholder="rangePlaceholder">
<template #renderExtraFooter>
<span>extra footer</span>
</template>
</DatePicker>
<DatePicker type="month" placeholder="请选择月份">
<template #renderExtraFooter>
<span>extra footer</span>
</template>
</DatePicker>
</Space>
<h2 class="mt30 mb10">三种大小</h2>
<Space vertical>
<RadioGroup v–model:value="sizeValue" :options="sizeOptions" button button–style="solid" />
<DatePicker v–model:value="sizeCaseValue" :size="sizeValue" placeholder="请选择日期" />
<DatePicker v–model:value="sizeMonthValue" type="month" :size="sizeValue" placeholder="请选择月份" />
<DatePicker v–model:value="sizeRangeValue" type="daterange" :size="sizeValue" :placeholder="rangePlaceholder" />
<DatePicker v–model:value="sizeWeekValue" type="week" :size="sizeValue" placeholder="请选择周" />
</Space>
<h2 class="mt30 mb10">定制日期单元格</h2>
<p class="mb10"
>用 <code>dateRender</code> 自定义日期单元格的内容与样式,入参 <code>current</code> 为该格日期的零点时间戳</p
>
<Space vertical>
<DatePicker v–model:value="dateRenderValue" placeholder="请选择日期">
<template #dateRender="{ current }">
<div :style="firstDayCellStyle(getDate(current))">{{ getDate(current) }}</div>
</template>
</DatePicker>
<DatePicker v–model:value="dateRenderRangeValue" type="daterange" :placeholder="rangePlaceholder">
<template #dateRender="{ current }">
<div :style="firstDayCellStyle(getDate(current))">{{ getDate(current) }}</div>
</template>
</DatePicker>
</Space>
<h2 class="mt30 mb10">自定义日期范围选择</h2>
<p class="mb10">
用两个独立的选择器组合出范围选择:<code>disabledDate</code> 按整日互相约束起止、<code>disabledTime</code>
把同一天的时分秒精确到边界之后,<code>open</code>
与展开事件联动,起点选完自动展开终点
</p>
<Space vertical>
<DatePicker
v–model:value="startValue"
type="datetime"
:disabled–date="disabledStartDate"
:disabled–time="disabledStartTime"
placeholder="开始日期"
@open–change="onStartOpenChange"
/>
<DatePicker
v–model:value="endValue"
v–model:open="endOpen"
type="datetime"
:disabled–date="disabledEndDate"
:disabled–time="disabledEndTime"
placeholder="结束日期"
/>
</Space>
<h2 class="mt30 mb10">后缀图标</h2>
<DatePicker v–model:value="suffixValue" placeholder="请选择日期">
<template #suffixIcon>
<svg width="1em" height="1em" viewBox="0 0 1024 1024" fill="currentColor" focusable="false" aria–hidden="true">
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</template>
</DatePicker>
<h2 class="mt30 mb10">自定义状态</h2>
<Space vertical>
<DatePicker v–model:value="errorValue" status="error" placeholder="请选择日期" />
<DatePicker v–model:value="warningValue" status="warning" placeholder="请选择日期" />
<DatePicker v–model:value="errorRangeValue" type="daterange" status="error" :placeholder="rangePlaceholder" />
<DatePicker v–model:value="warningRangeValue" type="daterange" status="warning" :placeholder="rangePlaceholder" />
</Space>
<h2 class="mt30 mb10">无边框</h2>
<Space vertical>
<DatePicker v–model:value="borderlessValue" :bordered="false" placeholder="请选择日期" />
<DatePicker v–model:value="borderlessWeekValue" type="week" :bordered="false" placeholder="请选择周" />
<DatePicker v–model:value="borderlessMonthValue" type="month" :bordered="false" placeholder="请选择月份" />
<DatePicker v–model:value="borderlessQuarterValue" type="quarter" :bordered="false" placeholder="请选择季度" />
<DatePicker v–model:value="borderlessYearValue" type="year" :bordered="false" placeholder="请选择年份" />
<DatePicker
v–model:value="borderlessDateRangeValue"
type="daterange"
:bordered="false"
:placeholder="rangePlaceholder"
/>
<DatePicker
v–model:value="borderlessWeekRangeValue"
type="weekrange"
:bordered="false"
:placeholder="weekRangePlaceholder"
/>
<DatePicker
v–model:value="borderlessMonthRangeValue"
type="monthrange"
:bordered="false"
:placeholder="monthRangePlaceholder"
/>
<DatePicker
v–model:value="borderlessQuarterRangeValue"
type="quarterrange"
:bordered="false"
:placeholder="quarterRangePlaceholder"
/>
<DatePicker
v–model:value="borderlessYearRangeValue"
type="yearrange"
:bordered="false"
:placeholder="yearRangePlaceholder"
/>
</Space>
<h2 class="mt30 mb10">弹出位置</h2>
<Space>
<DatePicker v–model:value="placementBottomLeftValue" placement="bottomLeft" placeholder="左下" />
<DatePicker v–model:value="placementBottomRightValue" placement="bottomRight" placeholder="右下" />
<DatePicker v–model:value="placementTopLeftValue" placement="topLeft" placeholder="左上" />
<DatePicker v–model:value="placementTopRightValue" placement="topRight" placeholder="右上" />
<DatePicker
v–model:value="placementRangeBottomLeftValue"
type="daterange"
placement="bottomLeft"
:placeholder="rangePlaceholder"
/>
<DatePicker
v–model:value="placementRangeTopLeftValue"
type="daterange"
placement="topLeft"
:placeholder="rangePlaceholder"
/>
</Space>
<h2 class="mt30 mb10">键盘操作</h2>
<p class="mb10">
面板展开后按 <code>Tab</code> 把键盘交给面板,方向键按当前面板层级的粒度移动光标、<code>Enter</code>
提交;带时间的形态下 <code>Tab</code> 在日期与时间两列之间切换,非活动列淡化
</p>
<Space vertical>
<DatePicker v–model:value="keyboardValue" placeholder="请选择日期" />
<DatePicker v–model:value="keyboardRangeValue" type="daterange" :placeholder="rangePlaceholder" />
<DatePicker v–model:value="keyboardDateTimeValue" type="datetime" placeholder="请选择日期时间" />
</Space>
<h2 class="mt30 mb10">按段默认时刻</h2>
<p class="mb10">
<code>defaultTime</code> 指定空草稿时的默认时分秒,范围形态可传两段元组按段指定(起点 <code>00:00:00</code>、终点
<code>23:59:59</code>);某段为 <code>undefined</code> 时该段退回当前时刻
</p>
<DatePicker
v–model:value="defaultTimeRangeValue"
type="datetimerange"
:default–time="rangeDefaultTime"
:placeholder="rangePlaceholder"
/>
</div>
</template>
评论前必须登录!
注册