"use client"; import Link from "next/link"; import { CSSProperties, useMemo, useState } from "react"; import calendarData from "../data/seasonal-calendar.json"; import styles from "./SeasonalCalendar.module.css"; type CategoryId = "fruit" | "seafood" | "activity"; type Category = { id: CategoryId; name: string; icon: string; color: string; soft: string }; type CalendarEvent = { id: string; title: string; category: CategoryId; start: string; end: string; region: string; bestTime: string; summary: string; tips: string; }; type CalendarDay = { date: Date; key: string; inMonth: boolean }; const categories = calendarData.categories as Category[]; const events = calendarData.events as CalendarEvent[]; const eventPalette: Record = { "fruit-peach": { color: "#df6548", soft: "#fff0e9" }, "fruit-grape": { color: "#9a6a31", soft: "#fbf1df" }, "fruit-fig": { color: "#a75278", soft: "#faedf3" }, "fruit-pear": { color: "#799536", soft: "#f0f6df" }, "seafood-crab": { color: "#267aa5", soft: "#e8f5fb" }, "seafood-urchin": { color: "#4f68b8", soft: "#edf1ff" }, "seafood-bass": { color: "#278b83", soft: "#e8f7f4" }, "seafood-shrimp": { color: "#46799c", soft: "#eaf3f8" }, "activity-buy": { color: "#6c57c8", soft: "#f0edff" }, "activity-menu": { color: "#a151b5", soft: "#f8ecfb" }, }; const eventImages: Record = { "fruit-peach": "/seasonal/honey-peach-pixel.png", }; export default function SeasonalCalendarPage() { const initial = parseDate(calendarData.defaultDate); const [month, setMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1)); const [selectedDate, setSelectedDate] = useState(calendarData.defaultDate); const [selectedEventId, setSelectedEventId] = useState(null); const [enabledCategories, setEnabledCategories] = useState>(() => new Set(categories.map((item) => item.id))); const days = useMemo(() => makeCalendarDays(month), [month]); const visibleEvents = useMemo(() => events.filter((event) => enabledCategories.has(event.category)), [enabledCategories]); const selectedEvents = useMemo(() => visibleEvents.filter((event) => event.start <= selectedDate && event.end >= selectedDate), [selectedDate, visibleEvents]); const selectedEvent = selectedEventId ? events.find((event) => event.id === selectedEventId) ?? null : null; const monthEvents = useMemo(() => { const first = formatDate(new Date(month.getFullYear(), month.getMonth(), 1)); const last = formatDate(new Date(month.getFullYear(), month.getMonth() + 1, 0)); return visibleEvents.filter((event) => event.start <= last && event.end >= first); }, [month, visibleEvents]); const changeMonth = (offset: number) => { const next = new Date(month.getFullYear(), month.getMonth() + offset, 1); setMonth(next); setSelectedDate(formatDate(next)); }; const selectDay = (day: CalendarDay) => { setSelectedDate(day.key); if (!day.inMonth) setMonth(new Date(day.date.getFullYear(), day.date.getMonth(), 1)); }; const toggleCategory = (id: CategoryId) => setEnabledCategories((current) => { const next = new Set(current); if (next.has(id)) { next.delete(id); if (selectedEvent?.category === id) setSelectedEventId(null); } else next.add(id); return next; }); const goToday = () => { const today = new Date(); setMonth(new Date(today.getFullYear(), today.getMonth(), 1)); setSelectedDate(formatDate(today)); }; return (
Seasonal CalendarWBBR STYLE TEMPLATE
本地 JSON 演示

SEASONAL SCHEDULE

{calendarData.title}

{calendarData.subtitle}。选择任意日期,即可查看当天正在进行的全部事项。
本月事项{monthEvents.length}跨日期事件
选中日期{selectedEvents.length}当天有效事项
数据分类{enabledCategories.size}当前显示分类
{month.getFullYear()}年 {month.getMonth() + 1}月{month.toLocaleDateString("zh-CN", { month: "long", year: "numeric" })}
{categories.map((category) => )}
{selectedEvent &&
正在突出显示{selectedEvent.title}{shortDate(selectedEvent.start)} — {shortDate(selectedEvent.end)},覆盖日期已统一着色
}
{["周一", "周二", "周三", "周四", "周五", "周六", "周日"].map((day) => {day})}
{days.map((day) => { const dayEvents = visibleEvents.filter((event) => event.start <= day.key && event.end >= day.key); const isToday = day.key === formatDate(new Date()); const isEventRange = Boolean(selectedEvent && selectedEvent.start <= day.key && selectedEvent.end >= day.key); return
selectDay(day)} style={isEventRange && selectedEvent ? eventStyle(selectedEvent) : undefined}>
{dayEvents.slice(0, 3).map((event) => { return ; })} {dayEvents.length > 3 && 还有 {dayEvents.length - 3} 项}
; })}

SEASON OVERVIEW

本月时令与事项清单

共 {monthEvents.length} 项 · 当前筛选结果
{monthEvents.map((event) => { const category = categories.find((item) => item.id === event.category)!; return ; })}
); } function parseDate(value: string) { const [year, month, day] = value.split("-").map(Number); return new Date(year, month - 1, day); } function formatDate(date: Date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function makeCalendarDays(month: Date): CalendarDay[] { const first = new Date(month.getFullYear(), month.getMonth(), 1); const mondayOffset = (first.getDay() + 6) % 7; const start = new Date(first.getFullYear(), first.getMonth(), 1 - mondayOffset); return Array.from({ length: 42 }, (_, index) => { const date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + index); return { date, key: formatDate(date), inMonth: date.getMonth() === month.getMonth() }; }); } function formatChineseDate(value: string) { const date = parseDate(value); return `${date.getMonth() + 1}月${date.getDate()}日`; } function weekdayName(value: string) { return ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][parseDate(value).getDay()]; } function shortDate(value: string) { const [, month, day] = value.split("-"); return `${Number(month)}月${Number(day)}日`; } function eventStyle(event: CalendarEvent) { const fallback = categories.find((item) => item.id === event.category)!; const palette = eventPalette[event.id] ?? fallback; return { "--event-color": palette.color, "--event-soft": palette.soft } as CSSProperties; }