"use client"; /** * Calendar - 课案日历组件 * * 维护者:ai13(teacher-portal) * 关联:lesson-plans/calendar 页面复用 * * 特性: * - 月视图,支持前后月切换 * - 日期点击回调 * - 日期标记(有课案的日期高亮) * - 纯 CSS + var(--*) 设计令牌 */ import { useState, useMemo, useCallback } from "react"; export interface CalendarEvent { date: string; // ISO date string YYYY-MM-DD title: string; type?: "lesson" | "exam" | "meeting"; } export interface CalendarProps { /** 初始月份(默认当前月) */ initialDate?: Date; /** 事件列表 */ events?: CalendarEvent[]; /** 日期点击回调 */ onDateClick?: (date: Date) => void; /** 月份切换回调 */ onMonthChange?: (date: Date) => void; } const WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"]; const WEEKDAYS_EN = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function formatDate(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function getEventColor(type: CalendarEvent["type"]): string { switch (type) { case "exam": return "var(--color-danger)"; case "meeting": return "var(--color-warning)"; case "lesson": default: return "var(--color-accent)"; } } export function Calendar({ initialDate = new Date(), events = [], onDateClick, onMonthChange, }: CalendarProps): React.ReactElement { const [currentDate, setCurrentDate] = useState(initialDate); const eventsByDate = useMemo(() => { const map = new Map(); for (const evt of events) { const existing = map.get(evt.date) || []; existing.push(evt); map.set(evt.date, existing); } return map; }, [events]); const days = useMemo(() => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startWeekday = firstDay.getDay(); const daysInMonth = lastDay.getDate(); const cells: Array<{ date: Date | null; isCurrentMonth: boolean }> = []; // 上月填充 for (let i = 0; i < startWeekday; i++) { cells.push({ date: null, isCurrentMonth: false }); } // 当月 for (let d = 1; d <= daysInMonth; d++) { cells.push({ date: new Date(year, month, d), isCurrentMonth: true }); } // 下月填充(补齐 6 行 = 42 格) while (cells.length < 42) { cells.push({ date: null, isCurrentMonth: false }); } return cells; }, [currentDate]); const handlePrevMonth = useCallback(() => { const prev = new Date( currentDate.getFullYear(), currentDate.getMonth() - 1, 1, ); setCurrentDate(prev); onMonthChange?.(prev); }, [currentDate, onMonthChange]); const handleNextMonth = useCallback(() => { const next = new Date( currentDate.getFullYear(), currentDate.getMonth() + 1, 1, ); setCurrentDate(next); onMonthChange?.(next); }, [currentDate, onMonthChange]); const today = formatDate(new Date()); return (
{/* 月份导航 */}

{currentDate.getFullYear()} 年 {currentDate.getMonth() + 1} 月

{/* 星期表头 */}
{WEEKDAYS.map((day, i) => (
{day}
))}
{/* 日期网格 */}
{days.map((cell, i) => { if (!cell.date) { return
; } const dateStr = formatDate(cell.date); const dayEvents = eventsByDate.get(dateStr) || []; const isToday = dateStr === today; return ( ); })}
); } export { WEEKDAYS, WEEKDAYS_EN, formatDate }; export default Calendar;