民國年日期選擇器(React)
民國/西元一鍵切換的 React 日期選擇器:年月日格狀月曆+「民國 115 年」年份下拉,可直接輸入「115/08/18」或「2026-08-18」文字解析(含 IME 檢查),輸出 ISO 日期並同時回傳民國與西元兩種寫法。
台灣表單(生日、證件效期、保單)習慣用民國年,但後端要的是西元 ISO 字串。這個元件把兩者接起來:文字輸入框可直接打「115/08/18」「2026-08-18」「民國115年8月18日」甚至「1150818」,失焦或按 Enter 就解析(中文輸入法組字中的 Enter 不會誤觸發);右側按鈕開啟月曆彈層,頂部「民國|西元」segmented 切換時年份標籤即時換算(民國=西元−1911,民國前的年份直接顯示西元不換算),搭配年份下拉、月份箭頭、7 欄格狀日期、今天標記與 min/max 停用範圍。value 永遠是 ISO 字串(YYYY-MM-DD),onChange 同時給民國、西元與依當前曆制格式化的顯示字串。
載入預覽⋯
npx shadcn@latest add https://webberui.com/r/roc-date-picker.jsonPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<RocDatePicker />
安裝
npx shadcn@latest add https://webberui.com/r/roc-date-picker.json或在 components.json 設定 registries 後,改用 @webberui/roc-date-picker 安裝。
使用
import {
RocDatePicker,
formatRocDate,
parseRocDateInput,
} from "@/components/ui/roc-date-picker";
const [iso, setIso] = React.useState("2026-08-18");
<RocDatePicker
value={iso}
onChange={(next, meta) => {
setIso(next); // "2026-08-18";清除時為 ""
if (meta) {
console.log(meta.roc); // { year: 115, month: 8, day: 18 }
console.log(meta.gregorian); // { year: 2026, month: 8, day: 18 }
console.log(meta.display); // 民國模式 "115/08/18"、西元模式 "2026/08/18"
}
}}
label="保單生效日"
min="2026-01-01"
max="2027-12-31"
/>;
// 解析與格式化函式可單獨使用(例如匯入舊資料、列印時顯示民國年)
parseRocDateInput("115/08/18"); // "2026-08-18"
parseRocDateInput("民國115年8月18日"); // "2026-08-18"
parseRocDateInput("2026-08-18"); // "2026-08-18"
formatRocDate("2026-08-18", "roc"); // "115/08/18"
formatRocDate("2026-08-18", "gregorian", "-"); // "2026-08-18"Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | string | null | — | 受控值:ISO 日期字串(YYYY-MM-DD),空字串或 null 表示未選;不提供時為非受控模式 |
defaultValue | string | "" | 非受控模式的初始值(ISO 字串) |
onChange | (iso: string, meta: RocDateChangeMeta | null) => void | — | 值變動時回呼:第一參數為 ISO 字串(清除時為空字串),第二參數同時給民國、西元與顯示字串(清除時為 null) |
calendar | "roc" | "gregorian" | — | 受控曆制:民國(roc)或西元(gregorian);不提供時由元件內部的切換鈕管理 |
defaultCalendar | "roc" | "gregorian" | "roc" | 非受控模式的初始曆制 |
onCalendarChange | (calendar: RocCalendar) => void | — | 使用者按下「民國|西元」切換時回呼 |
weekStartsOn | 0 | 1 | 0 | 一週從週日(0)或週一(1)開始 |
min | string | — | 可選的最早日期(ISO 字串,含當日);更早的日期停用、輸入時視為超出範圍 |
max | string | — | 可選的最晚日期(ISO 字串,含當日);更晚的日期停用、輸入時視為超出範圍 |
label | string | "日期" | 欄位標籤文字 |
placeholder | string | — | 輸入框佔位文字;未提供時依曆制顯示「例:115/08/18」或「例:2026/08/18」 |
disabled | boolean | false | 是否停用 |
name | string | — | 表單欄位名稱:以隱藏 input 送出 ISO 字串(顯示用的輸入框不帶 name) |
className | string | — | 透傳到最外層容器 |
RocDateChangeMeta
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
roc | RocDate | — | 民國年月日(西元 1911 年以前的日期 year 會是 0 或負數) |
gregorian | RocDate | — | 西元年月日 |
display | string | — | 依當前曆制格式化的顯示字串(「115/08/18」或「2026/08/18」) |
RocDate
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
year | number | — | 年(依上下文為民國年或西元年) |
month | number | — | 月(1~12) |
day | number | — | 日(1~31) |
具名匯出的函式
parseRocDateInput(raw: string): string | null——把使用者輸入的文字解析為 ISO 字串,失敗回傳null(規則見下方「細節」)formatRocDate(iso: string, calendar?: RocCalendar, separator?: string): string——依曆制格式化 ISO 日期,預設民國模式、分隔符「/」;ISO 不合法回傳""toRocDate(iso: string): RocDate | null——ISO 字串轉民國年月日fromRocDate(roc: RocDate): string | null——民國年月日轉 ISO 字串;日期不存在(如 2 月 30 日)回傳null
另外匯出 RocCalendar("roc" | "gregorian")型別,方便在表單層共用同一套換算邏輯。
細節
- 文字解析規則:接受「115/08/18」「115-8-18」「115.08.18」「民國115年8月18日」「1150818」與「2026-08-18」「2026/08/18」「2026年8月18日」「20260818」,全形數字與全形分隔符會先正規化。年份大於等於 1000 視為西元,否則視為民國(+1911);帶「民國」前綴一律視為民國年;8 碼純數字視為西元
YYYYMMDD,6~7 碼視為民國(Y)YYMMDD - 解析失敗或超出
min/max時輸入框標紅、顯示原因並設定aria-invalid,原本的值不會被清掉;輸入框清空後失焦則視為清除 - 民國元年=西元 1912 年。民國模式下遇到 1911 年(含)以前的日期,年份標籤、下拉選單與顯示字串都直接用西元,不會出現「民國 −3 年」;
meta.roc.year仍會如實回傳 0 或負數方便你自行判斷 - 年份下拉預設涵蓋民國元年到 2100 年;有
min/max時縮限為兩者的年份區間,月份箭頭在邊界月停用 - 「今天」標記與「今天」按鈕依使用者裝置時間、在掛載後才計算,SSR 與客戶端首屏一致;今天不在可選範圍時按鈕停用
- 月曆彈層以彈簧動畫開合、換月時新舊月份水平交錯滑動(
AnimatePresence mode="popLayout"),選中格以 scale 彈跳強調;網格固定 6 列 42 格,換月時高度不跳動 - 提供
name時會另外渲染一個隱藏 input 送出 ISO 字串,顯示用的輸入框不帶name,原生表單拿到的永遠是YYYY-MM-DD
可及性
- 輸入框以
label關聯,提示/錯誤訊息置於aria-describedby指向的role="status"(aria-live="polite")區域,解析結果會被螢幕閱讀器朗讀;解析失敗同步設定aria-invalid - 觸發按鈕為
type="button",帶aria-haspopup="dialog"、aria-expanded與aria-controls;彈層為role="dialog",Esc 關閉並把焦點還給觸發鈕,點擊元件外或焦點離開時自動收合 - 月曆採
role="grid"/row/columnheader/gridcell,選中日aria-selected、今天aria-current="date"、超出範圍aria-disabled;每一格有完整的aria-label(年月日+星期+今天/不可選) - 網格採漫遊式 tabindex:只有一格可被 Tab 到;方向鍵上下左右移動一天/一週、Home/End 跳到該週首尾、PageUp/PageDown 換月(加 Shift 換年)、Enter 或空白鍵選取;跨月時檢視月份自動跟著切換
- 曆制切換為
role="group"內的兩顆aria-pressed按鈕,年份下拉是原生select,鍵盤與螢幕閱讀器行為與一般表單控制項一致 - 輸入框按 Enter 解析前會檢查
isComposing與keyCode 229,中文輸入法組字中不會誤送出 - 使用者系統開啟「減少動態效果」時,彈層開合、換月滑動、選中彈跳與提示文字過場一律改為即時切換,只保留狀態與顏色變化