obsidian使用技巧
可视化年度日历:日记分类自动高亮,一览所有重要日子
插件ID:%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%8E%86%E6%97%A5%E8%AE%B0%E5%88%86%E7%B1%BB%E8%87%AA%E5%8A%A8%E9%AB%98%E4%BA%AE%E4%B8%80%E8%A7%88%E6%89%80%E6%9C%89%E9%87%8D%E8%A6%81%E6%97%A5%E5%AD%90
%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%8E%86%E6%97%A5%E8%AE%B0%E5%88%86%E7%B1%BB%E8%87%AA%E5%8A%A8%E9%AB%98%E4%BA%AE%E4%B8%80%E8%A7%88%E6%89%80%E6%9C%89%E9%87%8D%E8%A6%81%E6%97%A5%E5%AD%90
%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%8E%86%E6%97%A5%E8%AE%B0%E5%88%86%E7%B1%BB%E8%87%AA%E5%8A%A8%E9%AB%98%E4%BA%AE%E4%B8%80%E8%A7%88%E6%89%80%E6%9C%89%E9%87%8D%E8%A6%81%E6%97%A5%E5%AD%90:这是一个年度视角的日记日历,自动追踪指定目录的日记记录,把全年日记记录展示在一张日历视图中。
可视化年度日历:日记分类自动高亮,一览所有重要日子
本脚本受 Reddit 上 这篇帖子 启发制作,感谢原作者。
这是一个年度视角的日记日历,自动追踪指定目录的日记记录,把全年日记记录展示在一张日历视图中,可以直观地看到:
一年中,哪些日子写过日记,哪些日子被重点标记。

1.1 日历状态说明
- 灰色日期(普通) 这一天没有日记 也没有被手动标记
- 绿色日期:有日记 这一天存在对应的日记文件 无论内容多少,只要文件存在,就会自动高亮
- 紫色日期:手动标记
表示这一天对你有特殊意义
与是否写日记无关
点一下 → 变成紫色
再点一次 → 取消标记
标记信息会自动保存在当前笔记的属性
calToggles中(如果点太多乱了,可手动修改这里的数值) 适合用来标记:- 重要事件
- 特殊心境
- 值得回看的日子
- 今天 会有轻微描边,便于快速定位
- 未来日期 样式更淡,表示尚未发生
1.2 右键点击某一天
作用:打开或创建当天的日记
- 如果日记已存在 → 直接打开
- 如果不存在 → 自动创建日记文件并打开
1.3 使用方法
- 新建一个笔记,并将以下内容作为文件的属性放在顶部:
---
calYear: 2026
calToggles:
---
- 然后添加以下 dataviewJS 代码块
脚本会扫描 00Journal/01DailyNotes/ (可自行修改)及其所有子文件夹。
右键点击新建日记
- 脚本会根据日期自动创建年份和月份文件夹
- 并按照 文件名格式 创建日记,例如:
00Journal/01DailyNotes/2026/01/03_20260114.md
文件名格式
WW_YYYYMMDD例:03_20260114.mdWW_MMDDYYYY例:03_01142026.mdWW可以是任意周编号或前缀,系统只识别日期部分
```dataviewjs
// Mini year calendar (custom journal structure)
// - Left click: toggle day (stored in this note's frontmatter calToggles)
// - Right click: open/create journal note
// - Auto-highlight days with existing journal files
//
// JOURNAL STRUCTURE:
// 00Journal/01DailyNotes/YYYY/MM/WW_YYYYMMDD.md
//
// NOTES:
// - WW = ISO week number (2 digits)
// - Date parsing ONLY relies on YYYYMMDD part
// - Year / Month folders are auto-created
// --- 安全检测 ---
if (!dv.current() || !dv.current().file) {
dv.paragraph("⚠️ 日历只能在编辑模式下显示");
return;
}
const NOTE = dv.current().file.path;
const today = window.moment().startOf("day");
// ==========
// CONFIG
// ==========
const CFG = {
journalRoot: "00Journal/01DailyNotes",
journalExt: ".md",
enableLeftClickToggle: true,
enableRightClickOpenCreate: true,
openInNewPane: true,
disallowFutureRightClick: false,
};
// ==========
// ---------- Year resolution ----------
function asYear(v) {
if (!v) return Number(window.moment().format("YYYY"));
if (typeof v === "object" && typeof v.format === "function")
return Number(v.format("YYYY"));
if (v instanceof Date)
return Number(window.moment(v).format("YYYY"));
const s = String(v);
const y = parseInt(s.slice(0, 4), 10);
return Number.isFinite(y) ? y : Number(window.moment().format("YYYY"));
}
const year = asYear(dv.current().calYear ?? dv.current().calMonth);
// ---------- Frontmatter toggles ----------
function getToggles(frontmatter, monthStr) {
const map = frontmatter.calToggles ?? {};
const arr = map[monthStr] ?? [];
return new Set(arr.map(Number));
}
async function toggleDay(monthStr, dayNum) {
const file = app.vault.getAbstractFileByPath(NOTE);
await app.fileManager.processFrontMatter(file, fm => {
fm.calToggles = fm.calToggles ?? {};
fm.calToggles[monthStr] = fm.calToggles[monthStr] ?? [];
const arr = fm.calToggles[monthStr].map(Number);
const idx = arr.indexOf(dayNum);
if (idx >= 0) arr.splice(idx, 1);
else arr.push(dayNum);
arr.sort((a, b) => a - b);
fm.calToggles[monthStr] = arr;
});
}
// ---------- DOM helper ----------
function el(tag, className, text) {
const n = document.createElement(tag);
if (className) n.className = className;
if (text !== undefined) n.textContent = text;
return n;
}
// ---------- Journal path ----------
function journalPathFor(dateMoment) {
const y = dateMoment.format("YYYY");
const m = dateMoment.format("MM");
const ymd = dateMoment.format("YYYYMMDD");
const ww = dateMoment.isoWeek().toString().padStart(2, "0");
return \`${CFG.journalRoot}/${y}/${m}/${ww}_${ymd}${CFG.journalExt}\`;
}
// ---------- Parse journal filename ----------
function parseJournalName(name, targetYear) {
// Expect: WW_YYYYMMDD
const m = name.match(/^(\d{2})_(\d{8})$/);
if (!m) return null;
const y = Number(m[2].slice(0, 4));
const mo = Number(m[2].slice(4, 6));
const d = Number(m[2].slice(6, 8));
if (y !== targetYear) return null;
if (mo < 1 || mo > 12) return null;
if (d < 1 || d > 31) return null;
const iso = \`${y}-${String(mo).padStart(2, "0")}-${String(d).padStart(2, "0")}\`;
const mom = window.moment(iso, "YYYY-MM-DD", true);
if (!mom.isValid()) return null;
return {
monthStr: \`${y}-${String(mo).padStart(2, "0")}\`,
dayNum: d,
};
}
// ---------- Open or create journal ----------
async function openOrCreateJournal(dateMoment) {
const path = journalPathFor(dateMoment);
let file = app.vault.getAbstractFileByPath(path);
if (!file) {
const y = dateMoment.format("YYYY");
const m = dateMoment.format("MM");
const yearFolder = \`${CFG.journalRoot}/${y}\`;
const monthFolder = \`${yearFolder}/${m}\`;
if (!app.vault.getAbstractFileByPath(yearFolder)) {
await app.vault.createFolder(yearFolder);
}
if (!app.vault.getAbstractFileByPath(monthFolder)) {
await app.vault.createFolder(monthFolder);
}
const content = \`---\ndate: ${dateMoment.format("YYYY-MM-DD")}\n---\n\n\`;
file = await app.vault.create(path, content);
}
const leaf = CFG.openInNewPane
? app.workspace.getLeaf(true)
: app.workspace.getLeaf(false);
await leaf.openFile(file);
}
// ---------- Scan journals ----------
function getJournalDaysByMonth(rootPath, targetYear) {
const map = new Map();
const pages = dv.pages(\`"${rootPath}"\`);
for (const p of pages) {
const parsed = parseJournalName(p.file.name, targetYear);
if (!parsed) continue;
if (!map.has(parsed.monthStr)) {
map.set(parsed.monthStr, new Set());
}
map.get(parsed.monthStr).add(parsed.dayNum);
}
return map;
}
const journalMap = getJournalDaysByMonth(CFG.journalRoot, year);
// ---------- Rendering ----------
function renderMonth(parent, monthStr, activeSet, entrySet) {
const m0 = window.moment(\`${monthStr}-01\`, "YYYY-MM-DD", true);
if (!m0.isValid()) return;
const daysInMonth = m0.daysInMonth();
const firstDowMonFirst = (m0.day() + 6) % 7;
const weekLabels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const monthWrap = el("div", "mini-cal-month");
parent.appendChild(monthWrap);
monthWrap.appendChild(el("div", "mini-cal-title", m0.format("MMMM")));
const head = el("div", "mini-cal-head");
for (const w of weekLabels) head.appendChild(el("div", "mini-cal-dow", w));
monthWrap.appendChild(head);
const grid = el("div", "mini-cal-grid");
monthWrap.appendChild(grid);
for (let i = 0; i < firstDowMonFirst; i++) {
grid.appendChild(el("div", "mini-cal-empty", ""));
}
for (let d = 1; d <= daysInMonth; d++) {
const date = window.moment(
\`${monthStr}-${String(d).padStart(2, "0")}\`,
"YYYY-MM-DD",
true
);
const isToday = date.isSame(today, "day");
const isFuture = date.isAfter(today, "day");
let cls = "mini-cal-day";
if (entrySet.has(d)) cls += " has-entry";
if (activeSet.has(d)) cls += " is-on";
if (isToday) cls += " is-today";
if (isFuture) cls += " is-future";
const btn = el("button", cls, String(d));
btn.type = "button";
if (CFG.enableLeftClickToggle) {
btn.addEventListener("click", async ev => {
ev.preventDefault();
ev.stopPropagation();
await toggleDay(monthStr, d);
activeSet.has(d) ? activeSet.delete(d) : activeSet.add(d);
btn.classList.toggle("is-on");
});
}
if (CFG.enableRightClickOpenCreate) {
btn.addEventListener("contextmenu", async ev => {
ev.preventDefault();
ev.stopPropagation();
if (CFG.disallowFutureRightClick && isFuture) return;
await openOrCreateJournal(date);
});
}
grid.appendChild(btn);
}
}
// ---------- Render year ----------
dv.container.innerHTML = "";
dv.container.classList.add("mini-cal");
dv.container.appendChild(el("div", "mini-cal-year-title", String(year)));
const yearWrap = el("div", "mini-cal-year");
dv.container.appendChild(yearWrap);
for (let m = 1; m <= 12; m++) {
const monthStr = \`${year}-${String(m).padStart(2, "0")}\`;
const active = getToggles(dv.current(), monthStr);
const entries = journalMap.get(monthStr) ?? new Set();
renderMonth(yearWrap, monthStr, active, entries);
}
\`\`\`
```
1. CSS 样式代码
```
/* =========================
基础日历样式
========================= */
.mini-cal-year-title {
font-weight: 700;
margin-bottom: 10px;
font-size: 30px;
}
.mini-cal-year {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: 40px;
align-items: start;
}
.mini-cal-title {
font-weight: 600;
margin-bottom: 10px;
padding-left: 3px;
}
.mini-cal-head {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
margin-bottom: 10px;
}
.mini-cal-dow {
font-size: 0.75em;
opacity: 0.25;
text-align: center;
}
.mini-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
}
/* =========================
Mini-cal-day 基础
========================= */
.mini-cal-day {
position: relative; /* ⚠ 必须保留 */
aspect-ratio: 1 / 0.95;
height: auto;
border-radius: 6px;
border: 1px solid rgb(59,61,60);
background-color: rgb(59,61,60) !important;
box-shadow: none !important;
color: rgb(170,172,171) !important;
cursor: pointer;
padding: 0;
}
.mini-cal-day:hover {
color: rgb(222,224,223) !important;
background-color: rgb(69,71,70) !important;
border: inherit;
}
/* =========================
Today 高亮(极简,跨主题)
========================= */
body.theme-dark .mini-cal-day.is-today {
border-color: rgba(255, 255, 255, 0.85) !important;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35) !important;
}
body.theme-light .mini-cal-day.is-today {
border-color: rgba(0, 0, 0, 0.55) !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) !important;
}
.mini-cal-day.is-today:hover {
box-shadow: 0 0 0 1px currentColor !important;
}
/* =========================
状态样式:手动标记 / 已有日记
========================= */
/* 手动标记 */
.mini-cal-day.is-on {
background-color: var(--text-accent) !important;
border-color: transparent !important;
color: rgb(244,242,243) !important;
}
/* 已有日记 */
.mini-cal-day.has-entry {
background-color: rgb(38, 110, 65) !important; /* 深绿,高对比 */
color: rgb(220, 255, 230) !important;
border-color: rgb(48, 140, 85);
}
.mini-cal-day.has-entry:hover {
background-color: rgb(48, 140, 85) !important;
color: rgb(235, 255, 240) !important;
}
/* 已有日记 + 手动标记(紫色优先) */
.mini-cal-day.has-entry.is-on {
background-color: var(--text-accent) !important;
border-color: transparent !important;
color: rgb(244,242,243) !important;
}
/* 未来日期 */
.mini-cal-day.is-future {
border: 1px dashed rgb(76,78,77);
background-color: transparent !important;
color: rgb(130,132,131) !important;
}
.mini-cal-day.is-future:hover {
color: rgb(180,182,181) !important;
}
```
讨论
若阁下有独到的见解或新颖的想法,诚邀您在文章下方留言,与大家共同探讨。
反馈交流
其他渠道
版权声明
版权声明:所有 PKMer 文章如果需要转载,请附上原文出处链接。