⭐ 习惯工作台

📅 今日

今天完成情况

const HABIT_DIR = "复盘/习惯/Habits";
const LOG_PATH = "复盘/习惯/Habit-Log.md";
 
const habits = dv.pages(`"${HABIT_DIR}"`)
  .where(p => p.type === "habit").array()
  .map(p => ({ name: p.file.name, icon: p.icon ?? "•", order: Number(p.order ?? 99) }))
  .sort((a, b) => a.order - b.order);
 
const file = app.vault.getAbstractFileByPath(LOG_PATH);
const raw = file ? await app.vault.cachedRead(file) : "";
const log = {};
for (const line of raw.split(/\r?\n/)) {
  const m = line.match(/^\s*-\s*(\d{4}-\d{2}-\d{2})\s+(.*)$/);
  if (!m) continue;
  const rec = {};
  const re = /([^\s::]+)\s*[::]\s*(✅|🟡|❌)/g;
  let x;
  while ((x = re.exec(m[2])) !== null) rec[x[1]] = x[2];
  if (Object.keys(rec).length) log[m[1]] = rec;
}
 
const pad = n => String(n).padStart(2, "0");
const key = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const now = new Date(); now.setHours(0, 0, 0, 0);
const todayKey = key(now);
const rec = log[todayKey] ?? {};
const done = habits.filter(h => rec[h.name] === "✅").length;
const half = habits.filter(h => rec[h.name] === "🟡").length;
 
const parts = habits.map(h => {
  const v = rec[h.name];
  const t = v === "✅" ? "✅" : v === "🟡" ? "🟡" : v === "❌" ? "❌" : "·";
  return `<span class="wb-pill">${h.icon} ${h.name} ${t}</span>`;
}).join("");
 
const box = dv.el("div", "");
box.innerHTML = `
  <div style="font-size:1.05em;margin-bottom:8px"><b>${done}</b> / ${habits.length} 完成${half ? ` (+${half} 个半程)` : ""}
  <span class="wb-muted" style="float:right">${todayKey}</span></div>
  <div>${parts}</div>
  <div class="wb-muted" style="margin-top:8px">${log[todayKey] ? "今天的记录已存在,改符号即可。" : "今天还没记录 → 去 Habit-Log 加一行。"}</div>`;

📊 本月战绩

完成率 · 目标进度 · 连续天数

const HABIT_DIR = "复盘/习惯/Habits";
const LOG_PATH = "复盘/习惯/Habit-Log.md";
const SCORE = { "✅": 1, "🟡": 0.5, "❌": 0 };
 
const habits = dv.pages(`"${HABIT_DIR}"`)
  .where(p => p.type === "habit").array()
  .map(p => ({ name: p.file.name, icon: p.icon ?? "•", target: Number(p.target ?? 30), order: Number(p.order ?? 99) }))
  .sort((a, b) => a.order - b.order);
 
const file = app.vault.getAbstractFileByPath(LOG_PATH);
const raw = file ? await app.vault.cachedRead(file) : "";
const log = {};
for (const line of raw.split(/\r?\n/)) {
  const m = line.match(/^\s*-\s*(\d{4}-\d{2}-\d{2})\s+(.*)$/);
  if (!m) continue;
  const rec = {};
  const re = /([^\s::]+)\s*[::]\s*(✅|🟡|❌)/g;
  let x;
  while ((x = re.exec(m[2])) !== null) rec[x[1]] = x[2];
  if (Object.keys(rec).length) log[m[1]] = rec;
}
 
const pad = n => String(n).padStart(2, "0");
const key = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const now = new Date(); now.setHours(0, 0, 0, 0);
const shift = n => { const d = new Date(now); d.setDate(d.getDate() + n); return d; };
const monthKey = key(now).slice(0, 7);
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const elapsed = Math.floor((now - monthStart) / 86400000) + 1;
 
const rows = habits.map(h => {
  let full = 0, half = 0;
  for (const [k, v] of Object.entries(log)) {
    if (!k.startsWith(monthKey)) continue;
    if (v[h.name] === "✅") full++;
    else if (v[h.name] === "🟡") half++;
  }
  const score = full + half * 0.5;
  const rate = Math.round(score / elapsed * 100);
  let streak = 0;
  for (let i = 0; i < 400; i++) {
    const v = log[key(shift(-i))]?.[h.name];
    if (i === 0 && v === undefined) continue;
    if (v === "✅" || v === "🟡") streak++; else break;
  }
  const cells = [];
  for (let i = 6; i >= 0; i--) {
    const v = log[key(shift(-i))]?.[h.name];
    const c = v === "✅" ? "#0e9f8f" : v === "🟡" ? "#f0c24a" : v === "❌" ? "#efc0b9" : "#e9f1f0";
    cells.push(`<span style="display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:2px;background:${c}"></span>`);
  }
  const pct = Math.min(100, Math.round(score / h.target * 100));
  return `<tr>
    <td style="text-align:left;white-space:nowrap">${h.icon} ${h.name}</td>
    <td>${cells.join("")}</td>
    <td>${full}${half ? ` +${half}🟡` : ""} / ${elapsed}</td>
    <td>${rate}%</td>
    <td style="min-width:86px"><progress value="${score}" max="${h.target}"></progress><br><span class="wb-muted">${score} / ${h.target}</span></td>
    <td>${streak > 0 ? `🔥 ${streak} 天` : "—"}</td>
  </tr>`;
}).join("");
 
const el = dv.el("div", "");
el.innerHTML = `<table class="wb-matrix" style="width:100%;border-collapse:collapse">
  <thead><tr><th>习惯</th><th>近7天</th><th>本月</th><th>完成率</th><th>目标进度</th><th>连续</th></tr></thead>
  <tbody>${rows}</tbody></table>
  <div class="wb-muted" style="margin-top:8px">本月已过 ${elapsed} 天|完成率 = (✅×1 + 🟡×0.5) ÷ 已过天数</div>`;

🔥 近 30 天

每日整体完成度

const HABIT_DIR = "复盘/习惯/Habits";
const LOG_PATH = "复盘/习惯/Habit-Log.md";
const SCORE = { "✅": 1, "🟡": 0.5, "❌": 0 };
 
const habits = dv.pages(`"${HABIT_DIR}"`).where(p => p.type === "habit").array();
const file = app.vault.getAbstractFileByPath(LOG_PATH);
const raw = file ? await app.vault.cachedRead(file) : "";
const log = {};
for (const line of raw.split(/\r?\n/)) {
  const m = line.match(/^\s*-\s*(\d{4}-\d{2}-\d{2})\s+(.*)$/);
  if (!m) continue;
  const rec = {};
  const re = /([^\s::]+)\s*[::]\s*(✅|🟡|❌)/g;
  let x;
  while ((x = re.exec(m[2])) !== null) rec[x[1]] = x[2];
  if (Object.keys(rec).length) log[m[1]] = rec;
}
 
const pad = n => String(n).padStart(2, "0");
const key = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const now = new Date(); now.setHours(0, 0, 0, 0);
const shift = n => { const d = new Date(now); d.setDate(d.getDate() + n); return d; };
 
let cells = "";
let active = 0;
for (let i = 29; i >= 0; i--) {
  const k = key(shift(-i));
  const r = log[k];
  let v = null;
  if (r) {
    const vals = habits.map(h => SCORE[r[h.name]]).filter(x => x !== undefined);
    if (vals.length) v = vals.reduce((a, b) => a + b, 0) / habits.length;
  }
  if (v !== null) active++;
  const c = v === null ? "#eef4f3" : v >= 0.8 ? "#0b9c8c" : v >= 0.5 ? "#6cc9bb" : v > 0 ? "#c6e6e1" : "#f3cdc7";
  cells += `<span title="${k}" style="background:${c}"></span>`;
}
 
const el = dv.el("div", "");
el.innerHTML = `<div class="wb-heat">${cells}</div>
  <div class="wb-muted" style="margin-top:8px">近 30 天有 ${active} 天记录 · 颜色越深完成度越高 · 灰 = 未记录</div>`;