最近给博客的日历页面加了个备忘功能。效果是这样的:点击任意日期会显示当天的备忘事项,数据存储在 JSON 文件中,更换浏览器也能查看。

博客基于 Astro 框架构建,日历组件使用 Svelte 开发,样式用 Tailwind CSS。选择这个组合的原因是:
首先定义备忘的数据结构。一个备忘需要包含:唯一 ID、内容、日期、创建和更新时间。
// src/components/calendar/types.ts
export interface CalendarMemo {
id: string;
content: string;
date: string; // 格式:YYYY-MM-DD
createdAt: number; // 时间戳
updatedAt: number; // 时间戳
}
同时扩展日历格子的类型,让它能显示备忘信息:
export interface CalendarGridCell {
day: number;
dateKey: string;
posts: CalendarPost[];
memos: CalendarMemo[]; // 新增:当天的备忘列表
hasPost: boolean;
hasMemo: boolean; // 新增:是否有备忘
postCount: number;
memoCount: number; // 新增:备忘数量
isToday: boolean;
isSelected: boolean;
isEmpty: boolean;
}
备忘数据存储在 JSON 文件中,参考了课程表的数据格式。相比 localStorage,这种方式的好处是:
// public/calendar/memos.json
{
"id": "calendar-memos",
"name": "日历备忘",
"version": 1,
"lastUpdated": 1704067200000,
"memos": [
{
"id": "memo-1",
"content": "发布新博客文章",
"date": "2025-01-15",
"createdAt": 1705276800000,
"updatedAt": 1705276800000
}
]
}
创建 API 端点来读取备忘数据:
// src/pages/api/calendar-memos.json.ts
import type { APIRoute } from "astro";
import fs from "node:fs/promises";
import path from "node:path";
const MEMOS_FILE_PATH = path.join(
process.cwd(),
"public",
"calendar",
"memos.json"
);
export const GET: APIRoute = async () => {
try {
const data = await fs.readFile(MEMOS_FILE_PATH, "utf-8");
return new Response(data, {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(
JSON.stringify({ memos: [] }),
{ headers: { "Content-Type": "application/json" } }
);
}
};
前端通过 API 加载备忘数据:
// src/components/calendar/hooks/useCalendar.ts
const MEMOS_API_URL = "/api/calendar-memos.json";
export async function loadMemosFromStorage(): Promise<CalendarMemo[]> {
if (typeof window === "undefined") return [];
try {
const response = await fetch(MEMOS_API_URL);
if (response.ok) {
const data = await response.json();
return data.memos || [];
}
} catch (error) {
console.error("Failed to load memos from API:", error);
}
return [];
}
export function buildMemoDateMap(
memos: CalendarMemo[]
): Record<string, CalendarMemo[]> {
const memoDateMap: Record<string, CalendarMemo[]> = {};
memos.forEach((memo) => {
if (!memoDateMap[memo.date]) {
memoDateMap[memo.date] = [];
}
memoDateMap[memo.date].push(memo);
});
return memoDateMap;
}
点击日期后,在日历下方展示该日期的备忘列表:
<!-- Calendar.svelte -->
<script>
// 加载备忘数据
async function loadMemos() {
allMemos = await loadMemosFromStorage();
memoDateMap = buildMemoDateMap(allMemos);
}
// 点击日期切换选中状态
function handleCellClick(dateKey: string) {
if (selectedDateKey === dateKey) {
selectedDateKey = null;
} else {
selectedDateKey = dateKey;
}
}
// 当前选中日期的备忘
const displayedMemos = $derived(
selectedDateKey ? memoDateMap[selectedDateKey] || [] : []
);
</script>
<!-- 备忘展示区域 -->
{#if displayedMemos.length > 0}
<div class="mt-4">
<div class="h-[1px] w-full bg-[var(--button-border-color)] mb-2"></div>
<div class="flex items-center gap-2 mb-2">
<Icon icon="material-symbols:event-note" class="text-amber-500 text-sm" />
<span class="text-sm font-medium text-[var(--text-color)]">
{selectedDateKey} 备忘
</span>
</div>
<div class="flex flex-col gap-2">
{#each displayedMemos as memo (memo.id)}
<div class="flex items-start gap-2 px-3 py-2 rounded-lg
bg-amber-500/10 border border-amber-500/20">
<Icon icon="material-symbols:check-circle"
class="text-amber-500 text-sm mt-0.5 shrink-0" />
<span class="text-sm text-[var(--text-color)]">{memo.content}</span>
</div>
{/each}
</div>
</div>
{/if}
有备忘的日期需要在日历上显示标记,让用户一眼就能看到。我在日期格子的底部加了个橙色小圆点,跟文章标记的蓝色圆点区分开。
<!-- CalendarGrid.svelte -->
<button class={getCellClass(cell)} onclick={() => handleCellClick(cell)}>
{cell.day}
<!-- 标记点:文章(蓝) + 备忘(橙) -->
{#if (cell.hasPost || cell.hasMemo) && !cell.isSelected}
<span class="absolute bottom-0.5 flex gap-0.5">
{#if cell.hasPost}
<span class="w-1 h-1 rounded-full bg-[var(--link-color)]"></span>
{/if}
{#if cell.hasMemo}
<span class="w-1 h-1 rounded-full bg-amber-500"></span>
{/if}
</span>
{/if}
</button>
为了方便管理备忘,我写了几个 Node.js 脚本:
# 添加备忘
pnpm newmemo "2026-06-20" "公休"
# 或者直接用 node
node scripts/add-memo.mjs "2026-06-20" "公休"
脚本内容:
// scripts/add-memo.mjs
import fs from 'fs';
import path from 'path';
function generateId() {
return `memo-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
function addMemo(dateStr, content) {
const data = loadMemos();
const newMemo = {
id: generateId(),
content: content.trim(),
date: normalizeDate(dateStr),
createdAt: Date.now(),
updatedAt: Date.now()
};
data.memos.push(newMemo);
data.lastUpdated = Date.now();
saveMemos(data);
console.log('✅ 备忘添加成功!');
}
pnpm listmemo
输出示例:
📅 日历备忘列表
==================================================
名称: 日历备忘
版本: 1
最后更新: 2026/5/16 14:30:00
备忘数量: 3
==================================================
📌 2025-01
1. [2025-01-15] 发布新博客文章
2. [2025-01-20] 更新项目文档
📌 2025-02
3. [2025-02-01] 备份数据库
==================================================
# 先查看列表
pnpm listmemo
# 删除指定序号
pnpm delmemo 1
日历的响应式主要靠这几个类:
w-full max-w-md → 移动端全宽,桌面端最大 448px
max-h-[80vh] → 最大高度限制,避免超出屏幕
overflow-y-auto → 内容过多时可滚动
p-4 → 移动端留边距,桌面端也适用
暗色模式通过 CSS 变量自动适配:
/* 亮色模式 */
--bg-color: #ffffff;
--text-color: #1a1a1a;
--button-border-color: #e5e5e5;
/* 暗色模式 */
--bg-color: #1a1a1a;
--text-color: #e5e5e5;
--button-border-color: #333333;
备忘展示区域用 bg-amber-500/10 和 border-amber-500/20,琥珀色在明暗模式下都能保持较好的可读性。
添加完功能后,在日历页面底部加了个简单的图例:
public/calendar/
└── memos.json # 备忘数据文件
src/pages/api/
└── calendar-memos.json.ts # API 端点
src/components/calendar/
├── types.ts # 类型定义
├── Calendar.svelte # 主组件
├── hooks/
│ └── useCalendar.ts # 工具函数 + 数据加载
└── components/
├── CalendarGrid.svelte # 日历格子
├── MonthPicker.svelte # 月份选择
└── YearPicker.svelte # 年份选择
scripts/
├── add-memo.mjs # 添加备忘脚本
├── list-memos.mjs # 列表备忘脚本
└── delete-memo.mjs # 删除备忘脚本
这个备忘功能实现起来不算复杂,主要是几个部分的组合:
如果想进一步扩展,可以考虑:
完整代码已经提交到仓库,有需要的可以参考实现。
正在加载评论...