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 FengMo WenXue