This commit is contained in:
oneao committed 2026-10-09 17:32:14 +08:00
1 parent e99a9fb274
commit 0be0b0767a
788 files changed
+112023 -14941

No files matched your search

@@ -0,0 +1,180 @@
---
title: DatePicker 日期选择
description: 日期 / 日期时间 / 范围选择与时间滚轮(DatePicker / RangePicker / TimeSelect)
---
# 日期选择
本页包含三个组件:`G3DatePicker`(单选日期 / 日期时间)、`G3RangePicker`(日期范围)、`G3TimeSelect`(时 / 分 / 秒滚轮)。
它们的绑定值**都是字符串**(`TimeSelect` 除外,它绑定 `Date`),因此可以直接和后端接口对接,不需要在业务层做序列化。
## 基础用法
<demo vue="date/date-basic.vue" />
## 精度与格式
`format` 同时决定三件事:**面板粒度、展示格式、绑定值格式的默认值**;`valueFormat` 只影响绑定值。
<demo vue="date/date-format.vue" />
| 写法 | 面板 | 绑定值 |
| --- | --- | --- |
| `format="YYYY"` | 年选择器 | `2026` |
| `format="YYYY-MM"` | 年月选择器 | `2026-10` |
| `format="YYYY-MM-DD"`(默认) | 日期网格 | `2026-10-08` |
| `format="YYYY-MM-DD HH:mm"` | 日期网格 + 时间列 + 确定按钮 | `2026-10-08 09:30` |
| `format="YYYY-MM-DD HH:mm"` + `value-format="YYYY-MM-DD HH:mm:ss"` | 同上 | `2026-10-08 09:30:00` |
支持的格式 token:`YYYY` `YY` `MM` `M` `MMM` `MMMM` `DD` `D` `Do` `dd` `ddd` `dddd` `HH` `H` `hh` `h` `mm` `m` `ss` `s` `SSS` `A` `a` `Z` `ZZ`。
## 禁用日期与清除
<demo vue="date/date-disabled.vue" />
`disabledDate` 接收 `Date`,返回 `true` 表示该日不可选(禁用的格子不可聚焦、不可点击、不参与 hover 预览)。`timeDefault` 只在「没有值时点开面板」时决定时间初值:`'start'` 当天 00:00、`'end'` 当天 23:59:59、不传用当前时刻。
## 手输与键盘微调
输入框可以直接键入,容错规则如下:
<demo vue="date/manual-input.vue" />
| 输入 | 结果 |
| --- | --- |
| `2026-10-08` / `2026/10/08` / `2026年10月8日` | 都能解析(分隔符互认) |
| `2026-10`(部分输入) | 解析为 `2026-10-01` |
| `2026-13-45` | 判定非法:红框 1.5 秒后回填已提交值,**不会写入绑定值** |
| 空串 | 视为清空(`''`),不是错误 |
键盘:`Esc` 取消编辑、`Enter` 提交并关闭、`↑ / ↓` 微调光标所在的段(年 / 月 / 日 / 时 / 分)、`Alt + ↓` 打开面板并把焦点交给日历。
## 日期范围
两次点击确定起止;第二次点击的位置比起点更早时会**自动交换**。只点了一次就关闭面板不会写入值。
<demo vue="date/range-basic.vue" />
## 时间滚轮
`TimeSelect` 绑定 `Date`,`fields` 决定显示哪几列,`hour12` 追加 am/pm 列。
<demo vue="date/time-select.vue" />
## API
### Props(DatePicker)
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string \| Date \| null` | `null` | 绑定值。字符串按 `valueFormat` 解析 |
| `format` | `string \| string[]` | `'YYYY-MM-DD'` | 展示格式与面板粒度 |
| `valueFormat` | `string \| string[]` | 跟随 `format` | 绑定值格式 |
| `clearable` | `boolean` | `true` | 有值时显示清除按钮 |
| `disabled` | `boolean` | `false` | 禁用态 |
| `disabledDate` | `(date: Date) => boolean` | — | 禁用某些日期 |
| `timeDefault` | `'' \| 'start' \| 'end'` | `''` | 无值时的默认时间 |
### Props(RangePicker)
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `Array<string \| Date \| null> \| null` | `null` | `[start, end]`,元素可为空串 |
| `format` | `string \| string[]` | `'YYYY-MM-DD'` | 展示格式与面板粒度 |
| `valueFormat` | `string \| string[]` | 跟随 `format` | 绑定值格式 |
| `clearable` | `boolean` | `true` | 显示清除按钮 |
| `disabled` | `boolean` | `false` | 禁用态 |
| `disabledDate` | `(date: Date) => boolean` | — | 禁用某些日期 |
### Props(TimeSelect)
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `Date \| null` | `null` | 当前时间;为空时以当前时刻为基准 |
| `fields` | `Array<'hour' \| 'minute' \| 'second'>` | `['hour','minute','second']` | 显示哪几列 |
| `hour12` | `boolean` | `false` | 12 小时制(小时列 1-12 + 追加 am/pm 列) |
| `lowerMeridiem` | `boolean` | `false` | 上下午显示小写 `am` / `pm` |
| `disabled` | `boolean` | `false` | 禁用态 |
### Events
| 组件 | 事件 | 参数 | 说明 |
| --- | --- | --- | --- |
| DatePicker | `update:modelValue` / `change` | `(value: string)` | 提交后的字符串值;清空为 `''` |
| RangePicker | `update:modelValue` / `change` | `(value: string[])` | `[start, end]`;清空为 `['', '']` |
| TimeSelect | `update:modelValue` | `(value: Date)` | 时间变化,恒为 `Date` |
### 类型定义
```ts
export type DateValue = string | Date | null | undefined
export type DateTimeDefault = '' | 'start' | 'end'
export type TimeField = 'hour' | 'minute' | 'second'
export interface DatePickerProps {
modelValue?: DateValue
format?: string | string[]
valueFormat?: string | string[]
clearable?: boolean
disabled?: boolean
disabledDate?: (date: Date) => boolean
timeDefault?: DateTimeDefault
}
export interface RangePickerProps {
modelValue?: Array<string | Date | null> | null
format?: string | string[]
valueFormat?: string | string[]
clearable?: boolean
disabled?: boolean
disabledDate?: (date: Date) => boolean
}
export interface TimeSelectProps {
modelValue?: Date | null
fields?: TimeField[]
hour12?: boolean
lowerMeridiem?: boolean
disabled?: boolean
}
```
### 工具函数
日期运算不依赖 dayjs,`@g3soft/ui` 导出了一组纯函数,业务可复用:
| 函数 | 签名 | 说明 |
| --- | --- | --- |
| `formatDate` | `(value, format?) => string` | 按 token 格式化 |
| `parseDate` | `(value, format?) => Date \| null` | 严格匹配 → 规范化文本 → 原生解析三级回退 |
| `parseDateInput` | `(text, format) => { date, isEmpty, isValid }` | 手输解析,区分「空」与「非法」 |
| `analyzeDateFormat` | `(format) => DateFormatConfig` | 解析出 `hasYear / hasTime / timeFields / hour12 …` |
| `normalizeDateValue` / `normalizeDateForFormat` | `(value, format) => Date \| null` | 值归一与按精度归零 |
| `getMonthMatrix` / `getMonthRows` | `(year, month) => Date[]` / `Date[][]` | 42 格月历(周日起始) |
| `startOfDay` / `endOfDay` / `isSameDay` / `isSameMonth` / `isToday` | — | 日粒度判断 |
| `compareDay` / `compareDate` / `isInRange` / `addMonth` / `combineDateTime` | — | 比较与合成 |
| `listDateSegments` / `resolveDateSegment` / `stepDateSegment` | — | 手输光标分段与微调 |
### 样式变量
面板形态(尺寸与颜色)都可通过同名 CSS 变量覆盖:
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `--g3-calendar-main-width` | `252px` | 日历主体定宽(7 列 × 36px)。定宽而非被输入框拉伸 |
| `--g3-calendar-height` | `292px` | 日历区与时间列共用的高度(表头 40 + 星期行 36 + 6 行 × 36) |
| `--g3-calendar-cell-size` | `24px` | 单元格内块尺寸:选中 / 今日方块的边长、范围背景带的厚度 |
| `--g3-time-col-width` | `56px` | 时间列每列宽度(时 / 分 / 秒各一列) |
| `--g3-time-item-height` | `28px` | 时间列单项高度 |
| `--g3-time-select-height` | `196px` | `G3TimeSelect` 独立使用时的整块高度(在面板内改由日历区高度决定) |
## 实现说明
- **零第三方依赖**:fms 版依赖 dayjs + 两个插件,这里用 `date/date-utils.ts` 自研实现(格式化 token、严格解析、段定位、微调步进),保持组件库只依赖 `vue`;
- **值的内部表示**:内部一律用 `Date` 运算,**对外一律字符串**。展示用 `format`、绑定值用 `valueFormat`,两者可以不同(展示到分钟、绑定到秒);
- **不污染绑定值**:手输过程只改本地文本,失焦 / 回车才提交;非法输入进错误态并回填旧值——表单里绝不允许出现「半天没法提交但看不出哪里错」的状态;
- **面板是草稿值**:有时间精度时,点选只改草稿、点「确定」才提交;没有时间精度时点选即提交并关闭。关闭面板会丢弃草稿;
- **定位复用 Popover**(`trigger="manual"`、`placement="bottom-start"`、`unwrapped`):弹窗内打开日期面板、滚动跟随、Esc 关闭这些行为与 Select 一致;但面板**不**跟随触发框宽度(Select 才用 `followTriggerWidth`),日历定宽 252px,输入框再宽也不会把日期格子撑散;
- **单元格形态对齐 antd**:内块是 24px 的**圆角方块**(不是圆形);「今日」用 1px 主色**描边**(不改文字色,选中时描边与填充叠加);范围高亮是厚度 24px 的**整条背景带**(起止两端各染半段、端点内块只在朝外一侧倒角),所以跨格的范围能连成一条连续色带;
- 键盘可达性:日历是 `role="grid"`,格子为 `role="gridcell"`,只有当前格子 `tabindex=0`(roving tabindex),`← → ↑ ↓ Home End PageUp PageDown` 都能操作,且会跳过禁用日期;底部时间列是 `role="listbox"`,`↑ ↓` 步进 1、`PageUp/PageDown` 步进 5。