calendar —— 日历
本頁內容尚未翻譯。
日历是常驻的日期面板:date-picker 把面板藏在弹层里,calendar 把它摊开在页面上。它支持三种选择模式——单选、区间、多选——以及可选日期范围的约束。core/calendar 复用 date-picker 的网格算法(generateCalendarDays),在此之上叠加平移、范围约束、选择态切换。
- 各端实现
LhCalendar:日程视图、签到打卡、酒店/机票日期选择页。 - 需要日期常驻可见而非弹出选择的场景。
- 不该用它:空间受限的表单内(用
date-picker,弹层更省地方);只选一个日期且页面已很拥挤时。
import { getCalendarClassNames, shiftCalendarDate, shiftCalendarMonth, isCalendarDateDisabled, clampDateToRange, isCalendarSelected, normalizeCalendarSpan, isCalendarInSpan, toggleCalendarDate, getCalendarCellAria, generateCalendarDays, isSameDay,} from '@lh-ui/share/core/calendar'import type { LhCalendarRange, LhCalendarSelection } from '@lh-ui/share/core/calendar'依赖方向是语义性的:calendar 从 date-picker 引入 generateCalendarDays,而不是从 core/shared/date 重新实现——日历网格算法是 date-picker 的职责,calendar 是它的日历视图。而通用原语(compareDate / isDateDisabled / isSameDay)则直接走共享内核,calendar 不反向依赖 date-picker 的 logic。这是「通用原语下沉、领域算法归位」的分层约定。
反序区间的容错:isCalendarDateDisabled 和 clampDateToRange 都先调 normalizeCalendarSpan 归一区间——用户传 [end, start](反序)不会导致全禁用或钳制错误。UI 层不强制调用方先排序。
shiftCalendarMonth 的归一:year * 12 + month + delta 再拆回年月,((total % 12) + 12) % 12 处理负数取模——delta 为负跨年时(如 1 月 -2)月份不会变成 -1。
多选的不可变切换:toggleCalendarDate 命中则移除、否则加入并按时间排序——返回新数组,直接可用于响应式更新,且顺序稳定(避免 UI 乱跳)。
| 函数 | 签名 | 返回 | 作用 |
|---|---|---|---|
getCalendarClassNames |
() => string[] |
['lh-calendar'] |
容器类名 |
shiftCalendarDate |
(date, days) => Date |
新 Date | 天平移(跨月安全) |
shiftCalendarMonth |
(year, month, delta) => { year, month } |
年月 | 月平移 |
isCalendarDateDisabled |
(date, range?) => boolean |
是否禁用 | 无约束恒 false |
clampDateToRange |
(date, range?) => Date |
钳制后日期 | 越界拉回边界 |
isCalendarSelected |
(date, selected) => boolean |
是否选中 | 同日判定 |
normalizeCalendarSpan |
(a, b) => [Date, Date] |
归一区间 | 保证 start ≤ end |
isCalendarInSpan |
(date, start, end) => boolean |
是否在闭区间 | 任一端 null 返回 false |
toggleCalendarDate |
(dates, date) => Date[] |
新数组 | 命中移除/否则加入并排序 |
getCalendarCellAria |
(options: { date, selected, range? }) => LhAriaAttrs |
aria 属性包 | aria-current / aria-disabled |
export type LhCalendarRange = [Date, Date]export type LhCalendarSelection = 'single' | 'range' | 'multiple'网格数据结构复用 LhDatePickerDayData(见 date-picker)。
getCalendarCellAria 的输出很克制——只在有状态时输出属性(LhAriaAttrs 的假值省略约定):
getCalendarCellAria({ date: selectedDate, selected: selectedDate, range: undefined })// { 'aria-current': 'date' }
getCalendarCellAria({ date: new Date(2026, 0, 1), selected: null, range: [new Date(2026, 2, 1), new Date(2026, 3, 1)] })// { 'aria-disabled': true }
getCalendarCellAria({ date: someDate, selected: null })// {}(无状态时空对象)- 选中日用
aria-current="date"(而非aria-selected)——日历是导航式控件,APG 约定用 current 表达「当前位置」。 - 禁用日用
aria-disabled,UI 层的点击与键盘导航都要跳过这些格子。 - 键盘交互:聚焦格子后 ↑/↓/←/→ 用
shiftCalendarDate(date, ±1|±7)移动,Enter/Space 触发选择。 - 网格整体仍应挂
role="grid",与 date-picker 面板同一套结构。
import { shiftCalendarMonth, normalizeCalendarSpan, toggleCalendarDate, isCalendarInSpan,} from '@lh-ui/share/core/calendar'
shiftCalendarMonth(2026, 0, -2) // { year: 2025, month: 10 }(负数取模正确)shiftCalendarMonth(2026, 11, 1) // { year: 2027, month: 0 }normalizeCalendarSpan(new Date(2026, 3, 1), new Date(2026, 2, 1))// [2026-03-01, 2026-04-01](反序自动归一)isCalendarInSpan(new Date(2026, 2, 15), new Date(2026, 2, 1), new Date(2026, 2, 28)) // trueisCalendarInSpan(new Date(2026, 2, 15), null, null) // false(任一端 null)toggleCalendarDate([new Date(2026, 2, 1)], new Date(2026, 2, 5))// [2026-03-01, 2026-03-05](加入并排序)toggleCalendarDate([new Date(2026, 2, 1), new Date(2026, 2, 5)], new Date(2026, 2, 1))// [2026-03-05](命中移除)<!-- Vue3:区间模式 --><div :class="getCalendarClassNames()"> <div class="lh-calendar__header"> <button type="button" @click="shift(-1)">‹</button> <span class="lh-calendar__title">{{ year }} 年 {{ month + 1 }} 月</span> <button type="button" @click="shift(1)">›</button> </div> <div class="lh-calendar__body" role="grid"> <div v-for="row in weekRows(days)" :key="row[0].date.getTime()" class="lh-calendar__row" role="row"> <button v-for="cell in row" :key="cell.date.getTime()" type="button" class="lh-calendar__cell" :class="cellClass(cell)" role="gridcell" v-bind="getCalendarCellAria({ date: cell.date, selected: selected, range: allowedRange })" @click="onPick(cell.date)" > {{ cell.day }} </button> </div> </div></div>// 区间模式的两段式选择(UI 层规则,core 不内置)// if (!rangeStart || (rangeStart && rangeEnd)) { rangeStart = date; rangeEnd = null }// else rangeEnd = date (早于 rangeStart 时 normalizeCalendarSpan 会归一)// React:多选模式// const [dates, setDates] = useState<Date[]>([])// onClick: setDates(toggleCalendarDate(dates, cell.date))// uniapp:<view> 网格 + @touchstart;core 的 range 约束与选择切换函数照用| 类名 | 语义 |
|---|---|
.lh-calendar |
基础类 |
.lh-calendar__header / __title |
头部导航 |
.lh-calendar__body |
网格区 |
.lh-calendar__row |
一周 |
.lh-calendar__cell |
日期格 |
.lh-calendar__cell.is-today / .is-selected / .is-in-range / .is-start / .is-end / .is-disabled / .is-other-month |
状态修饰 |
选中 --lh-primary-color,区间段低透明度主色,今天描边,禁用 --lh-text-disabled。
- month 0 起:与 date-picker、原生
Date一致,UI 显示+1。 - 三种选择模式与 core 的关系:
LhCalendarSelection只是类型声明,core 的函数是模式无关的——isCalendarSelected管 single,isCalendarInSpan管 range,toggleCalendarDate管 multiple。模式切换的接线在 UI 层。 - 区间模式的半选态:
rangeStart已定、rangeEnd未定时,hover 的格子要实时高亮中间段(UI 层用临时 end 渲染),core 的generateCalendarDays接受 null 的 rangeEnd,不会报错。 clampDateToRange的用途:外部设值(如v-model传入范围外日期)时收敛回合法区间,UI 层 watch 值变化时调一次。- 常驻面板的性能:calendar 不像 date-picker 只在打开时算网格,翻月时重算即可;不要每帧重算
generateCalendarDays(它创建 35~42 个 Date 对象)。 - 与 date-picker 的边界:两者共用网格数据但不共用状态模型——date-picker 的
LhDatePickerState面向弹层交互,calendar 没有可见态。
@lh-ui/share/core/calendar → shiftCalendarMonth( )(填好参数后点运行)Copyright © 2024-2026 风默文学 | 宁ICP备2024006574号 |
苏公网安备32040002010537