云计算百科
云计算领域专业知识百科平台

Vue3日期选择器(DatePicker)

效果如下图:

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

在线预览

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 // (v-model:value) 当前取值;传入即受控
defaultValue?: PickerValue // 非受控初始值,未绑 v-model:value 时生效(与面板初始日期 defaultPickerValue 不同)
open?: boolean // (v-model:open) 面板是否展开;传入即受控
// 双向绑定
formattedValue?: PickerFormattedValue // 字符串轨道值:父组件传入时以它为准(受控),随选择经 update 事件回写
// 内容数据
type?: PickerType // 选择形态
format?: string // 展示格式,date-fns 占位符,默认随 type 变化
valueFormat?: string // 绑定值格式,默认与 format 相同
placeholder?: string | [string, string] // 输入框提示文字,范围形态可分别为两段指定
defaultPickerValue?: number // 面板初始日期(时间戳),默认取 value 或今天
startDayOfWeek?: StartDayOfWeek // 一周起始日,0 为周一
disabledDate?: (timestamp: number) => boolean // 不可选择的日期
disabledTime?: PickerDisabledTime | PickerRangeDisabledTime // 不可选择的时间,仅带时间形态生效(范围形态额外接收段标识)
defaultTime?: PickerDefaultTime // 空草稿时的默认时分秒(只取其中的时分秒),范围形态可按段指定
timePickerProps?: PickerTimePanelProps // 时间面板选项(步长 / 12 小时制 / 隐藏禁用项)
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' // 面板弹出位置
// 范围形态的面板指示箭头:不传时跟随弹出方位(仅左侧对齐的 bottomLeft / topLeft 展示),
// 传 true 始终展示、传 false 始终隐藏,仅范围形态生效
showArrow?: boolean
showToday?: boolean // 是否展示面板底部的「今天」快捷,面板切到周 / 月 / 季 / 年视图时隐藏
showNow?: boolean // 是否展示面板底部的「此刻」快捷,仅带时间形态生效
// 进阶透传
dateRender?: (params: PickerDateRenderParams) => VNode | VNode[] // 自定义日期单元格内容(替换日号),入参 current 为该格日期的零点时间戳
renderExtraFooter?: () => VNode | VNode[] // 面板底部的额外页脚,渲染在操作行之上
suffixIcon?: VNode | (() => VNode) // 自定义选择框后缀图标
panelClass?: string // 面板额外类名
panelStyle?: CSSProperties // 面板额外样式
zIndex?: number // 面板层级,优先级最高
}
/** 范围形态草稿变更的来源信息:本次改动落在哪一段 */
export interface DatePickerRangeInfo {
range: PickerRangeSide
}
export interface DatePickerSlots {
suffixIcon?: () => VNode[] // 自定义选择框后缀图标,优先于同名 prop
separator?: () => VNode[] // 范围形态两段之间的分隔符,默认右向箭头
dateRender?: (params: PickerDateRenderParams) => VNode[] // 自定义日期单元格内容(替换日号),优先于同名 prop
renderExtraFooter?: () => VNode[] // 面板底部的额外页脚,优先于同名 prop
}
/**
* 对外公开的值与配置类型
*
* 选择器内核(`components/picker`)不对外导出,这里按本组件文档所引用的类型名逐个别名转出,
* 使用方 `import type { DatePickerXxx } from 'vue-amazing-ui'` 即可获得对应提示
*/

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'
// value / open 为受控字段:prop 默认值必须是 undefined(布尔缺省会被强转为 false,导致判据恒为受控)
// 非受控初值由 useControlledModel 的第二个参数承担
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
/** 面板指示箭头的尺寸,与 `.datepicker-range-arrow` 的宽高保持一致 */
const RANGE_ARROW_SIZE = 16
/**
* 范围形态的浮层间距
*
* 需额外留出箭头所在的一段空白(箭头尺寸的 2 / 3),
* 否则 16px 的箭头会压住触发器的底边、盖掉输入区下沿。
*/

const RANGE_POPUP_OFFSET = POPUP_OFFSET + (RANGE_ARROW_SIZE * 2) / 3
/** 带面板指示箭头的弹出方位(仅左侧对齐的两个方位,其余方位只展示面板) */
const ARROW_PLACEMENTS: FloatingPlacement[] = ['bottomLeft', 'topLeft']
// 主题变量随组件壳下发,浮层经 Teleport 后不在壳内,需同样注入到面板上
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(' '))
// 显式标注插槽类型:`useSlots()` 的返回值参与 `mergedDateRender` / `mergedExtraFooter` 的推导,构成推断环
const slots: DatePickerSlots = useSlots()
const slotsExist = useSlotsExist(['dateRender', 'renderExtraFooter'])
/**
* 日期单元格内容定制:同名插槽优先于 prop,两轨统一为「入参 → VNode[]」的函数下发给内核
* (内核只收函数:VNode 需在其渲染上下文内创建,`computed` 里直接求值会产生「无主的 hoisted vnode」)
*/

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]
}
})
/** 面板底部的额外页脚:同名插槽优先于 prop,同样以渲染函数下发 */
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))
/** 展示与回写共用的格式:`valueFormat` 优先,未指定时跟随展示格式 */
const mergedValueFormat = computed(() => props.valueFormat ?? mergedFormat.value)
/** 输入框按格式长度估宽,避免浏览器按 20 字符默认估宽导致触发器明显偏宽 */
const inputSize = computed(() => getInputSize(mergedValueFormat.value))
/** 字符串轨道受控:父组件传入 `formattedValue` 时以它为准,与主轨道解耦 */
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')
/** 触发器上的激活段下标(0 为起点段) */
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
})
/** 单选形态下正在预览的时间戳(面板展开且悬浮了日期格),无预览为 null */
const singlePreviewTimestamp = computed<number | null>(() => {
// 依赖先读取(同 hoverPreviewIndex):条件短路会漏采依赖,悬浮变化便不再触发重算
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
})
/**
* 范围形态的面板禁用判定:激活段为起点时,本次展开中终点段被激活过且有值 → 禁用「晚于终点」的日期,
* 反之同理。用户传入的 `disabledDate` 始终优先
*/

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])]
}
// 悬浮面板日期时,当前激活段显示该日期的预览文本
// ⚠️ 依赖先读取(同 hoverPreviewIndex):条件短路会漏采 `hoverTimestamp`,悬浮不再触发重算
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]
})
/** 单段输入的提示文字(范围形态由 `placeholders` 承担,此处传空) */
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)
/**
* 是否展示面板指示箭头
*
* 不传 `showArrow` 时按「实际方位」判定,传值则以显式开关为准。
*/

const showRangeArrow = computed(() => {
if (!isRange.value || props.showArrow === false) return false
return props.showArrow === true || ARROW_PLACEMENTS.includes(actualPlacement.value)
})
/** 面板过渡:仅给 name,类名走 Vue 默认派生(-enter-from/-enter-active/…)。展开 / 收起的缩放原点
随**实际方位**取(见 `datepicker-zoom` 一节),过渡名因此与方位无关 —— 首帧方位未回填也不会取反向动效 */

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 }
)
// 承载层(Modal / Drawer / Dialog / 外层浮层面板)关闭时收起:容器关闭只是 v-show 隐藏、不派发任何
// 指针或焦点事件,面板会停在打开态并占着层级槽位,容器重开时以「仍打开」的样子出现。
// 收起走同一个入口,草稿复位由上面的 open watcher 承担
useContainerClose(() => requestOpen(false))
function requestOpen(next: boolean) {
if (fullyDisabled.value || openModel.value === next) return
openModel.value = next
emits('openChange', next)
}
/**
* 点击触发器只负责展开(展开态点击不收起,收起由外部点击 / Esc / 选中值触发):按落点确定本次会话的
* 激活段(点终点段即激活终点段,点空白区落到起点段)并把焦点移到该段输入框 —— 聚焦态由 focus 驱动,
* 面板收起后下划线不会随之淡出
*/

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)
}
/**
* 写入范围形态的某一段(面板选择与手输提交共用):起点晚于终点时丢弃另一端、只保留本次写入,
* 两段齐全(或未选段声明可为空)才对外提交;只写到一段时保持展开,由调用场景决定是否切到另一端。
* 返回写入后的区间与其文本(未提交时即为草稿),供 `ok` 事件复用
*/

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)
// 切到另一端时把焦点一并挪过去(等 DOM 更新后再聚焦),
// 否则下划线已落到另一端、输入光标还留在原段,接着输入会写进错误的一段
nextTick(() => triggerRef.value?.focus(otherIndex))
return { value: next, formatted: nextFormatted }
}
requestOpen(false)
return { value: next, formatted: nextFormatted }
}
/**
* 面板选择日期:写入当前激活段,并把激活段切到另一端以接着选第二段;
* 键盘导航同 {@link onSelect}(只把该段的草稿设为光标所在日期,不落值、不切段)
*/

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)
}
/**
* 日期时间范围的面板草稿变更:与单选日期时间形态同口径,面板内的选择(日期或时间)只落在草稿上、
* 点「确定」才提交;此处不做「起点晚于终点丢弃另一端」的纠正,纠正发生在提交时(与 `applyRangeSide` 同径)
*/

function onRangeDraftChange(timestamp: number, side: PickerRangeSide) {
const current = draftRange.value ?? rangeValue.value ?? [null, null]
draftRange.value = side === 'start' ? [timestamp, current[1]] : [current[0], timestamp]
}
/** 日期时间范围的「确定」:并入当前激活段并按面板选择同径提交,派发 `ok` */
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
}
/**
* 「确定」/「此刻」的提交:都回写双轨、派发 `change` 并收起面板(收起时草稿由 `open` 的监听统一丢弃);
* 差异仅在事件 —— `ok` 只由「确定」派发,「此刻」是「跳到当前时刻」的快捷入口、不派发 `ok`
*/

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)
}
/* 键盘导航 */
/** 面板的键盘入口:各面板经 `defineExpose` 暴露,组件壳层按当前形态把按键转给对应的面板 */
interface PanelKeydownTarget {
handleKeydown: (event: KeyboardEvent) => boolean
}
/**
* 键盘归属:`false` 为文本输入态(方向键移动输入光标),`true` 为面板操作态(方向键操作面板光标)。
* 两态由 Tab 切换:输入态下 Tab 只把键盘交给面板、焦点不动(否则第一下 Tab 就移出组件);面板操作态
* 下 Tab 在两个区之间循环,循环到头交还浏览器移出焦点。输入字符、点击输入区或重新聚焦即回到输入态。
*
* 面板操作态同时把输入框置为只读(插入光标不应再提示「可直接输入」,此刻键盘归面板所有)。
*/

const keyboardPanelMode = ref(false)
/** 键盘操作当前所在的区:仅带时间形态有两个区可取,其余形态恒为日期区(面板内部按该状态转发) */
const activeSection = ref<PickerActiveSection>(null)
/** 是否具备「日期 / 时间」两个可切换的区 */
const hasTimeSection = computed(() => isDateTime.value || isDateTimeRange.value)
/** 唤起面板的按键中不作数的一类:修饰键不是输入意图,Esc / 回车另有既有语义 */
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
})
// 切换激活段即重置活动区:Tab 的循环从「未指定」重新开始,与换手后的鼠标操作起点一致
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
}
/**
* Tab:切换键盘归属,并在面板内的两个区之间循环;循环到头(回到「无活动区」)时不阻止
* 默认行为,交还浏览器把焦点移出组件
*/

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">
/* 以下两类规则必须留在全局:它们作用于 <Popup> 宿主编译出的面板节点(scope id 属宿主),
且 Teleport 到 body 后不在本组件 DOM 子树内,scoped 与 :deep() 均选不中。
收口口径:① 面板几何以 .datepicker-panel-container 收口;② 动画类与关键帧统一加 `datepicker-` 前缀,避免与宿主页面撞名 */

.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)
// 周形态用例:面板按 ISO 周成行高亮,展示格式为 ISO 周年 + 周序号,带「周」字后缀
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())
// 周格式用例:周年占位符只能取 ISO 口径(RRRR)——与周序号(II)配对才可解析,详见下方 format 用例说明;该用例与官方一致取当天值
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)
// 月范围用例的预设:值同样支持时间戳与函数(此处为「近 N 个月」两个区间)
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)
// 按段默认时刻用例:范围形态按段指定(起点当日 0 点、终点当日 23:59:59)
const defaultTimeRangeValue = ref<[number, number] | null>(null)
/** 起点取当日 0 点、终点取当日 23:59:59(只取其中的时分秒,日期部分不参与落值) */
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)
// 日期时间用例:主用例 + 时间面板选项 / 12 小时制 / 隐藏「此刻」三个细项,初始值均为空
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)
}
/** 范围形态待选区间变化时的回调:`info.range` 标识本次改动落在哪一段 */
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)
}
/** 面板视图切换时的回调:`mode` 为切换后的视图层级 */
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)
// 隐藏禁用项用例:与「不可选择日期和时间」同源,改用 timePickerProps 把禁用的时分秒从面板中隐藏
const hideDisabledOptionsValue = ref<number | null>(null)
/** 不可选择今天之后的日期(`disabledDate` 入参为「当日零点」的毫秒时间戳) */
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()
}
/** 半开区间 [start, end) 的整数序列 */
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()
}
/** 禁用的时间区间:4-23 时 / 30-59 分 / 55-56 秒 */
function disabledDateTimeUnits() {
return {
disabledHours: () => range(4, 24),
disabledMinutes: () => range(30, 60),
disabledSeconds: () => [55, 56]
}
}
// 选择不超过七天的范围用例:初始为空值,可选范围由草稿区间动态限制
const sevenDaysValue = ref<[number, number] | null>(null)
/** 草稿区间:`calendarChange` 直接给出的时间戳区间 */
const sevenDaysDraft = ref<[number | null, number | null] | null>(null)
/**
* 草稿期间把可选日期限制在草稿两端 ±7 天内
*
* 草稿由 `calendarChange` 记录、`disabledDate` 读草稿动态收窄可选范围,草稿变化即重算禁用态。
*/

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)
/** 近 N 天区间:起点取 N 天前的零点、终点取当前时刻 */
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)
/** 定制日期单元格:每月 1 号描一圈边框(单元格内层块高 24px,自定义节点须按 border-box 等高,否则外框会把圆圈撑高 2px) */
function firstDayCellStyle(dayOfMonth: number): CSSProperties {
const cellStyle: CSSProperties = { boxSizing: 'border-box', height: '24px' }
return dayOfMonth === 1 ? { …cellStyle, border: '1px solid #1677ff', borderRadius: '50%' } : cellStyle
}
// 自定义日期范围选择用例:两个独立选择器经 disabledDate / disabledTime 与展开状态互相约束
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' }
]
/** 起点不得晚于终点的「整日」:同一天仍可选,越界的时间点交给 disabledTime */
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[]
}
/**
* 由边界时刻的时分秒构造禁用单位:逐级判定(只有高一级与边界相同,下一级才有额外禁用项),边界时刻本身视为越界
*
* @param direction `notLaterThan` 禁用早于等于边界的时刻(终点侧用)、`notEarlierThan` 禁用晚于等于边界的时刻(起点侧用)
*/

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) : []
}
}
/**
* 同一天内的时间约束:跨天则无额外禁用项(整日越界由内核按激活段禁用)
*
* @param boundary 约束边界的时刻(终点侧为起点、起点侧为终点)
* @param timestamp 面板当前所编辑的时刻
*/

function disabledUnitsNotLaterThan(boundary: number, timestamp: number) {
if (!isSameDay(timestamp, boundary)) {
return {}
}
return disabledTimeUnitsByBoundary(getHours(boundary), getMinutes(boundary), getSeconds(boundary), 'notLaterThan')
}
/** 同 disabledUnitsNotLaterThan,方向相反:禁用晚于边界时刻的时间单位 */
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>

赞(0)
未经允许不得转载:网硕互联帮助中心 » Vue3日期选择器(DatePicker)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!