可视化年度日历:日记分类自动高亮,一览所有重要日子

可视化年度日历:日记分类自动高亮,一览所有重要日子

本脚本受 Reddit 上 这篇帖子 启发制作,感谢原作者。

这是一个年度视角的日记日历,自动追踪指定目录的日记记录,把全年日记记录展示在一张日历视图中,可以直观地看到:

一年中,哪些日子写过日记,哪些日子被重点标记。

可视化年度日历:日记分类自动高亮,一览所有重要日子--

1.1 日历状态说明

  1. 灰色日期(普通) 这一天没有日记 也没有被手动标记
  2. 绿色日期:有日记 这一天存在对应的日记文件 无论内容多少,只要文件存在,就会自动高亮
  3. 紫色日期:手动标记 表示这一天对你有特殊意义 与是否写日记无关 点一下 → 变成紫色 再点一次 → 取消标记 标记信息会自动保存在当前笔记的属性 calToggles 中(如果点太多乱了,可手动修改这里的数值) 适合用来标记:
    • 重要事件
    • 特殊心境
    • 值得回看的日子
  4. 今天 会有轻微描边,便于快速定位
  5. 未来日期 样式更淡,表示尚未发生

1.2 右键点击某一天

作用:打开或创建当天的日记

  • 如果日记已存在 → 直接打开
  • 如果不存在 → 自动创建日记文件并打开

1.3 使用方法

  1. 新建一个笔记,并将以下内容作为文件的属性放在顶部:
---
calYear: 2026
calToggles:
---
  1. 然后添加以下 dataviewJS 代码块

脚本会扫描 00Journal/01DailyNotes/ (可自行修改)及其所有子文件夹。

右键点击新建日记

  • 脚本会根据日期自动创建年份和月份文件夹
  • 并按照 文件名格式 创建日记,例如: 00Journal/01DailyNotes/2026/01/03_20260114.md

文件名格式

  • WW_YYYYMMDD 例: 03_20260114.md
  • WW_MMDDYYYY 例: 03_01142026.md
  • WW 可以是任意周编号或前缀,系统只识别日期部分
```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 forum 论坛相关