⭐ 习惯工作台
🧭 一屏看完
📅 今日
今天完成情况
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>`;