---
description: "后台的时间形状：一屏铺开当月，每个日期落上属于它的事件，切月看往返、点日看详情。凡是「哪天要发生哪件事」的资源，就是它"
---

# 日历（月份网格 + 分类事件）

后台的时间形状：一屏铺开当月，每个日期落上属于它的事件，切月看往返、点日看详情。凡是「哪天要发生哪件事」的资源，就是它。

<iframe src="/skills/dashboard/assets/demo-calendar.html"
        style="width:100%;height:720px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：日历"></iframe>

试这几下：

- 点**上月 / 下月**在月份间穿梭；**今天**按钮一键回到当月并选中今日
- 点任意日期 —— 下方**详情面板**列出当日全部事件；今日是圆点高亮，选中格描蓝边
- 12 号那格塞了 4 条 —— 只显示前 3 条 + **+1 更多**，面板里能看全
- **新建**按选中日预填、按类型（会议 / 任务 / 截止）分色；**删除**先弹确认；每次变更都有 toast

## 规矩

- **可见月份是 state**，网格靠 `useMemo({year, month})` 派生；render 里不裸调 `new Date()`（SSR 不安全）。demo 只在启动时读一次 wall clock 拿"今天"和种子月，之后完全靠状态推进；真实版通常把 `year/month` 放 URL 上，刷新和分享都稳定
- **事件按日归位**，同格超出 `MAX_VISIBLE` 折成"+N 更多"，完整列表交给详情面板。demo 直接 filter 内存数组按日期字符串分组；真实版按可见月份的日期区间调 `Repository.list()`，服务端返回后再按天归组
- **月初对齐周首日**：`leading = (firstDayOfWeek + 6) % 7`（此处采用中文习惯周一为首日；蓝本英文版是周日为首），末尾补齐到 7 的倍数。前后 pad 出来的空格子是**常态**，不要用"空态"警示占位
- 破坏性操作（删除事件）先过确认弹窗，成功后 toast + 网格与面板同步刷新；真实版删除走一次 API 调用，成功后失效该月份的查询键

> 蓝本：`add-calendar.md`[^add-calendar]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-calendar.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-calendar.html"
    ```

[^add-calendar]: [`add-calendar.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-calendar.md)，2026-07-28 读。
