// AttendanceCalendar:月度考勤日历 // 依据:02-architecture-design.md §15.5 AttendanceCalendar 设计 // - 月历视图,每天用颜色点标记考勤状态 // - present=success / late=warning / absent=danger / leave=ink-subtle // - 支持月份导航(可选 month + onMonthChange props) "use client"; import { useMemo } from "react"; import type { AttendanceRecord } from "@/types"; import { cn } from "@/lib/utils"; interface AttendanceCalendarProps { records: AttendanceRecord[]; /** YYYY-MM 格式,未传则使用当前月 */ month?: string; /** 月份切换回调,传入则显示导航按钮 */ onMonthChange?: (month: string) => void; } const STATUS_STYLES: Record = { present: "bg-success", late: "bg-warning", absent: "bg-danger", leave: "bg-ink-subtle", }; const STATUS_LABELS: Record = { present: "出勤", late: "迟到", absent: "缺勤", leave: "请假", }; const WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"]; function parseMonth(month: string): { year: number; month: number } { const [yearStr, monthStr] = month.split("-"); return { year: parseInt(yearStr ?? "0", 10), month: parseInt(monthStr ?? "1", 10) - 1, }; } function formatMonth(year: number, month: number): string { return `${year}-${String(month + 1).padStart(2, "0")}`; } function shiftMonth(month: string, delta: number): string { const { year, month: m } = parseMonth(month); const date = new Date(year, m + delta, 1); return formatMonth(date.getFullYear(), date.getMonth()); } export function AttendanceCalendar({ records, month, onMonthChange, }: AttendanceCalendarProps) { const { year, month: monthLabel, days, recordMap, } = useMemo(() => { const now = new Date(); const target = month ? parseMonth(month) : { year: now.getFullYear(), month: now.getMonth() }; const y = target.year; const m = target.month; const firstDay = new Date(y, m, 1); const lastDay = new Date(y, m + 1, 0); const startWeekday = firstDay.getDay(); const totalDays = lastDay.getDate(); const dayCells: (number | null)[] = []; for (let i = 0; i < startWeekday; i++) dayCells.push(null); for (let d = 1; d <= totalDays; d++) dayCells.push(d); while (dayCells.length % 7 !== 0) dayCells.push(null); const map = new Map(); for (const r of records) { map.set(r.date, r); } return { year: y, month: m, days: dayCells, recordMap: map }; }, [records, month]); // 统计 const stats = useMemo(() => { const counts = { present: 0, late: 0, absent: 0, leave: 0 }; for (const r of records) { counts[r.status]++; } return counts; }, [records]); return (
{onMonthChange && ( )}

{year}年{monthLabel + 1}月考勤

{onMonthChange && ( )}
{(Object.keys(STATUS_LABELS) as AttendanceRecord["status"][]).map( (s) => ( {STATUS_LABELS[s]} ), )}
{/* 日历网格 */}
{WEEKDAYS.map((w) => (
{w}
))}
{days.map((day, idx) => { if (day === null) { return
; } const dateStr = `${year}-${String(monthLabel + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; const record = recordMap.get(dateStr); return (
{day} {record && ( )}
); })}
{/* 统计 */}
出勤 {stats.present} 迟到 {stats.late} 缺勤 {stats.absent} 请假 {stats.leave}
); } export default AttendanceCalendar;