PE02▸⌨️ Input · 進階輸入▸日期
Date Picker
日期選擇
難度 ★★★★★標籤 date · calendar · range3 min read
模式簡述
Date Picker:日期挑選。單日、區間、多選都要支援。i18n、時區、weekstart 都是雷區。
Claude Code Prompt
參考:PE02 date-picker 組件:DatePicker.tsx Date Picker:日期挑選。單日、區間、多選都要支援。i18n、時區、weekstart 都是雷區。 限制: - weekstart 寫死 - range 模式起訖點不對稱 - 時區漂移
完整程式碼
"use client";
import { useEffect, useState } from "react";
import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
type State = "closed" | "open" | "selected" | "range";
export function DatePicker({ state = "open" }: { state?: State }) {
const [open, setOpen] = useState(state !== "closed");
useEffect(() => setOpen(state !== "closed"), [state]);
const selected = state === "selected" ? [15] : state === "range" ? [10, 11, 12, 13, 14, 15] : [];
const days = Array.from({ length: 30 }, (_, i) => i + 1);
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
return (
<div className="w-full max-w-xs">
<div className="relative">
<button
onClick={() => setOpen((o) => !o)}
className="flex w-full items-center justify-between rounded-md border border-border bg-background/50 px-3 py-2 text-xs"
>
<span className={selected.length ? "text-foreground" : "text-muted-foreground"}>
{state === "range"
? "2026-05-10 → 2026-05-15"
: state === "selected"
? "2026-05-15"
: "選擇日期"}
</span>
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
</button>
{open && (
<div className="absolute inset-x-0 top-full z-10 mt-1 rounded-md border border-border bg-background p-3 shadow-xl">
<div className="mb-2 flex items-center justify-between">
<button className="p-1 hover:text-lab-accent">
<ChevronLeft className="h-3 w-3" />
</button>
<span className="text-xs font-medium">2026 年 5 月</span>
<button className="p-1 hover:text-lab-accent">
<ChevronRight className="h-3 w-3" />
</button>
</div>
<div className="grid grid-cols-7 gap-0.5 text-center font-mono text-[10px] text-muted-foreground">
{weekdays.map((w) => (
<div key={w} className="py-0.5">{w}</div>
))}
{days.map((d) => {
const sel = selected.includes(d);
const isStart = selected[0] === d;
const isEnd = selected[selected.length - 1] === d;
const inRange = selected.length > 1 && sel && !isStart && !isEnd;
return (
<button
key={d}
className={cn(
"rounded py-1 text-[11px] hover:bg-lab-accent/10",
sel && !inRange && "bg-lab-accent text-background",
inRange && "bg-lab-accent/20 text-foreground rounded-none",
isStart && selected.length > 1 && "rounded-r-none",
isEnd && selected.length > 1 && "rounded-l-none",
)}
>
{d}
</button>
);
})}
</div>
</div>
)}
</div>
</div>
);
}何時用
- ✅ 訂房、訂機票的入退日期
- ✅ 報表時間區間篩選
- ✅ 日程安排
何時別用
- ❌ 只需要文字輸入(input type=date)
- ❌ 未來/過去極遠日期(生日用 month/year drop)
- ❌ 需要時、分、秒(合併 time picker)
我踩過的坑
-
weekstart 寫死:美國週日開頭、台灣週一開頭。要可設定。
-
range 模式起訖點不對稱:選了 start 後 hover 應該 preview range。
-
時區漂移:使用者選 5/10 但存了 UTC 04-30 23:00。日期類用 ISO date string,不是 ISO datetime。
相關模式
- PA05
- PE04