
以前,每天早上我都被任务和念头淹没:现在该做什么、要回复谁、任务的优先级是什么,几天前我到底在想些什么?现在我只需打开一篇笔记,就能一眼看到我所有的项目、任务、习惯和想法。这篇文章里,我会用最简单的语言,一步一步详细讲解我整套每日笔记系统,让你一个晚上就能亲手搭起来。
为什么要在 Obsidian 里写日记
🤔 什么是 Obsidian(如果你是第一次听说)
Obsidian 是一款免费的笔记软件,它把所有内容以普通文本文件的形式存放在你的电脑上。
它与记事本或 Word 最大的区别在于,笔记之间可以用链接相互关联,就像互联网上的网页一样。最终你会得到一个随你一起成长的个人“知识库”。
Obsidian 还可以通过插件来扩展,插件是能增加新功能的小型附加组件。本文介绍的整套系统,正是建立在插件之上。
⬇️ 如何下载并安装 Obsidian(从零开始)
它是免费的,只需几分钟。
1. 下载程序。 打开官方网站 obsidian.md,点击大大的 Download 按钮。网站会自动识别你的系统(Windows、macOS 或 Linux)。手机上则在 App Store 或 Google Play 中搜索“Obsidian”。
2. 安装。 运行下载好的文件,像安装其他程序一样按提示完成安装。
3. 创建库(Vault)。 首次启动时,Obsidian 会让你创建一个库(Vault)——它其实就是电脑上的一个文件夹,所有笔记都存放在里面。点击“创建新库”,起个名字,再选择它在磁盘上的位置。
4. 完成。 现在你可以创建第一篇笔记了。本文用到的所有插件都在程序内部安装,我们会在第 1 步讲到。
⚠️ 请只从官方网站 obsidian.md 下载 Obsidian,这样你得到的才是最新且安全的版本。
💚 为什么用 Obsidian,而不用手机自带的备忘录
简单说,有三个原因:
一切都属于你,而且永久保存。 笔记以普通文本文件的形式保存在你的设备上。服务不会关停,你的数据也不会消失。哪怕用最简单的“记事本”也能打开。
笔记之间的联系。 想法不再是一张张孤立的纸页,而是通过链接连成一张完整的知识网络,久而久之就成了你的“第二大脑”。
灵活。 插件让你按自己的需要定制这个程序:从简单的日记,到本文这样复杂的任务系统。
纸质笔记本很棒。但它有一个缺点:你昨天写下的内容,永远停留在“昨天”。
任务不会自己顺延,习惯不会自动汇成图表,想法也会在翻页之间丢失。
在 Obsidian 里一切都不一样。每日笔记成为你一天的中心:
自动顺延,并来自各个项目
与 Google Calendar 同步
按钮和进度图表
自由记录灵感与思考的空间
早上醒来,我立刻就能想起昨天在忙什么。没完成的任务会自动顺延到今天。像习惯、冥想这样简单的日常事项,我只需点一下按钮就能标记,它们还会汇总成清晰的变化趋势。而新的任务,我直接从与目标相连的项目中获取。
这套简单的系统让我保持专注:我做真正需要做的事,而不是拖延。让我们一起来搭建它。
我的完整系统以及 PARA 结构的介绍,见本站之前的文章。
第 1 步:需要安装什么
要在 Obsidian 里记录任务、想法和习惯,我们需要几个插件和一项内置设置。
🧩 什么是插件,从哪里获取
插件是为 Obsidian 增加新功能的附加组件,有点像浏览器扩展。
Obsidian 本身能做的事不多:写笔记、把笔记链接起来。但按钮、图表、与日历同步,这些功能都是靠插件实现的。
所有插件都是免费的,在程序内点几下就能安装:设置 → 第三方插件 → 浏览 → 按名称搜索 → 安装 → 启用。
第三方插件
打开 设置 → 第三方插件 → 浏览,逐个安装:
| 插件 | 用途 |
|---|---|
| Dataview | 从笔记中汇总数据(例如按日期汇总想法、生成图表) |
| Meta Bind | 直接在正文中把笔记属性变成按钮和输入框 |
| Homepage | 创建一个启动时自动打开的主页 |
| Templater | 在创建笔记时运行脚本(用于顺延任务) |
| Buttons | 创建一键运行命令的按钮 |
| QuickAdd | 按钮的“大脑”:运行宏和脚本 |
安装 Dataview 后,进入它的设置,把所有复选框都勾选上(尤其是 Enable JavaScript Queries 和 Enable Inline JavaScript Queries),否则日历小组件无法工作。

启用内置的”日记”(Daily notes)
Obsidian 开箱即用就能写日记。我们进入 设置 → 核心插件,启用 日记(Daily notes)。然后按自己的习惯进行配置。
📅 什么是"日记"(Daily notes)
这是 Obsidian 的内置功能:每个日历日对应一篇笔记。
你点一下”今天”按钮,程序就会自己创建(或打开)一篇标题为今天日期的笔记,比如
20-06-2026。它的方便之处在于,每一天都有自己固定的”位置”。你永远知道去哪里找某个日期的记录,而且这些笔记会自动排成一本按时间顺序排列的日记。

三个关键设置:
-
笔记标题的格式。 我用的就是日、月、年:
DD-MM-YYYY这一点很重要:我所有的代码(小组件、任务结转)都是严格按
DD-MM-YYYY格式来查找笔记的。如果你想在标题里加上星期几,就加上相应的字母(例如dddd),但这样代码里的格式也要一起改。哪个字母代表什么,可以方便地在 format.cm(Moment.js 文档)上查到。 -
新笔记所在的文件夹。 我的日记放在
2. Areas/Дневники/Ежедневные заметки。我所有内容都按 PARA 结构整理,而写日记属于”生活领域”(Area),所以它的位置就在Areas文件夹里。
🗂️ 什么是 PARA(一种文件夹体系)
PARA 是一种简单的方法,把你所有的笔记分到四个文件夹里,这样什么都不会丢:
• Projects(项目)——你现在正在做的、有明确成果的事情。
• Areas(领域)——你需要持续维护的事情:健康、财务,当然也包括写日记。
• Resources(资源)——“留给将来”的、关于感兴趣主题的资料和知识。
• Archive(归档)——已经不再有用,但删掉又觉得可惜的东西。
日记是需要持续坚持的事情,所以它放在 Areas 文件夹里。
- 模板。 这是每篇新笔记创建时所依据的结构。我的模板在
0. Files/4. Templates/Шаблон ежедневных заметок。“Templates”的意思就是模板。
我的日记笔记结构是这样的:
属性 → 周/日历小组件 → 当天计划 → 任务结转 → 习惯 → 饮食 → 想法。 并不是每次都能全部填完。但这种努力本身,让我变得更自律了。
第 2 步:笔记的属性(YAML)
我们打开模板,从上到下过一遍。首先看到的是笔记的属性(也叫 YAML 或 frontmatter)。
它们有什么用?属性是笔记的”官方”数据,其他插件可以很方便地读取。比如,习惯图表的数据正是取自属性,而不是普通的正文。这条规则值得记住:对图表和自动化重要的内容,都存在属性里。
属性写在笔记的最上方,夹在两行三个短横线之间:
---
Спорт: false
Чтение: false
Прогулка: false
Завтрак:
Перекусы:
Обед:
Ужин:
Итого_ккал:
---这里有两组:
- 习惯(
Спорт、Чтение、Прогулка)——“复选框”类型。false表示”还没完成”。 - 饮食(
Завтрак、Обед、Ужин、Перекусы、Итого_ккал)——“文本”类型,在这里记录吃了什么,以及一个卡路里计数器。
添加自己的属性很简单:在属性模式下点击 "+ 添加属性",输入名称并选择类型(文本、数字、复选框、日期)。填上几天之后,你就能在图表上看到变化趋势了。

第 3 步:与 Google 日历和任务同步
在模板里,属性的正下方是一大段代码。别害怕——它是 AI 写的,我只是粘贴了进去。这段代码会绘制出一个漂亮的小组件:显示星期几、本周的进度,而在”任务”模式下,它会与你的 Google 日历和 Google Tasks 同步。直接在 Obsidian 里就能看到当天的事件和任务,可以标记完成,也可以添加新的。

要让它正常工作,光有模板里的代码还不够。你需要让 Obsidian 获得访问你 Google 账号的权限。这是通过免费的 Google Apps Script 服务来完成的。我们一步一步来,慢慢地、详细地过一遍。
☁️ 什么是 Google Apps Script,为什么需要它
Google Apps Script 是 Google 的一项免费服务,你可以在上面运行小程序,来操作你自己的 Google 服务(日历、Tasks、邮件)。
为什么这里要用到它?因为 Obsidian 不能直接查看你的 Google 日历。所以我们创建一个小小的”中间人”:一个运行在 Google 服务器上的程序,它能看到你的日历,并通过一个专用链接提供数据。
Obsidian 访问这个链接,就能拿到事件和任务的列表。你不需要会编程:只要粘贴现成的代码,再按下面的步骤一步步完成设置就行。
3.1. 在 scripts.google.com 上准备服务器
第 3 步是最容易被漏掉的。代码里用到了
Tasks.Tasklists.list(),它属于"高级服务"。如果你没有添加 Tasks API,脚本就会报错崩溃。
下面就是需要粘贴到编辑器里的代码(它负责读取事件/任务,以及创建/关闭任务):
// ======================================
// GOOGLE APPS SCRIPT — TASKS + CALENDAR
// ======================================
// Authorisation check (run once manually)
function testAuth() {
const taskLists = Tasks.Tasklists.list();
Logger.log(taskLists);
const events = CalendarApp.getDefaultCalendar().getEvents(new Date(), new Date());
Logger.log(events.length + " events today");
}
// ====== READING: calendar events + tasks for the day ======
function doGet(e) {
try {
const tz = Session.getScriptTimeZone();
// DEBUG mode: open GAS_URL?debug=1 in the browser — it will show ALL tasks
if (e && e.parameter && e.parameter.debug) {
const debugLists = (Tasks.Tasklists.list().items) || [];
const all = [];
debugLists.forEach(list => {
const items = (Tasks.Tasks.list(list.id, {
showCompleted: true, showHidden: true, maxResults: 100
}).items) || [];
items.forEach(t => {
all.push({ list: list.title, listId: list.id, title: t.title, due: t.due || null, status: t.status });
});
});
return ContentService
.createTextOutput(JSON.stringify({
success: true, scriptTimeZone: tz,
listsFound: debugLists.map(l => l.title),
totalTasks: all.length, all: all
}))
.setMimeType(ContentService.MimeType.JSON);
}
const param = (e && e.parameter && e.parameter.date) ? e.parameter.date : null;
const base = param
? new Date(+param.split("-")[0], +param.split("-")[1] - 1, +param.split("-")[2])
: new Date();
const start = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 0, 0, 0);
const end = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 23, 59, 59);
const dayStr = Utilities.formatDate(base, tz, "yyyy-MM-dd");
const events = CalendarApp.getDefaultCalendar().getEvents(start, end).map(ev => ({
title: ev.getTitle(),
allDay: ev.isAllDayEvent(),
time: ev.isAllDayEvent() ? null : Utilities.formatDate(ev.getStartTime(), tz, "HH:mm")
}));
const tasks = [];
const lists = (Tasks.Tasklists.list().items) || [];
const today = new Date();
const todayStr = Utilities.formatDate(today, tz, "yyyy-MM-dd");
const isToday = dayStr === todayStr;
lists.forEach(list => {
const items = (Tasks.Tasks.list(list.id, { showCompleted: true, showHidden: true }).items) || [];
items.forEach(t => {
if (!t.due) return;
const taskDay = t.due.substring(0, 10);
const isExactDay = taskDay === dayStr;
const isOverdue = isToday && taskDay < dayStr && t.status === "needsAction";
if (isExactDay || isOverdue) {
tasks.push({
id: t.id, listId: list.id, title: t.title,
status: t.status, completed: t.completed || null, overdue: isOverdue
});
}
});
});
tasks.sort((a, b) => (a.overdue === b.overdue) ? 0 : (a.overdue ? -1 : 1));
return ContentService
.createTextOutput(JSON.stringify({ success: true, date: dayStr, events, tasks }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// ====== WRITING: create OR close a task ======
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
if (data.action === "complete") {
const listId = data.listId || Tasks.Tasklists.list().items[0].id;
Tasks.Tasks.patch({ status: "completed" }, listId, data.taskId);
return ContentService
.createTextOutput(JSON.stringify({ success: true, message: 'Task done!' }))
.setMimeType(ContentService.MimeType.JSON);
}
const title = data.title;
const dueDate = data.dueDate;
const taskLists = Tasks.Tasklists.list();
const defaultTaskList = taskLists.items[0].id;
const task = Tasks.Tasks.insert({ title: title, due: dueDate, status: 'needsAction' }, defaultTaskList);
return ContentService
.createTextOutput(JSON.stringify({ success: true, taskId: task.id, message: 'Task added!' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}3.2. 首次授权
在你亲自允许之前,Google 不会让这段代码访问你的日历。我们先手动操作一次:
每个人的个人脚本都会出现"Google 尚未验证此应用"这个页面。你并没有把数据传给任何外部的人——这段代码只在你自己的账号内运行。

3.3. 部署以及该复制哪个链接
现在我们把这段代码变成一个真正的网址,供 Obsidian 访问:
• 执行身份:"我"(你的账号)
• 谁可以访问:"任何人"
/exec 结尾的那个链接。一定要复制 以
/exec结尾的链接。带有/dev的链接在 Obsidian 中无法使用。https://script.google.com/macros/s/AKfycbx…………/exec

3.4. 把小组件代码粘贴到模板中
现在我们把小组件代码粘贴到每日笔记模板里。这是一个 ```dataviewjs 代码块,它会绘制一张显示周数和日历的卡片。最重要的一点——把第一行引号里的链接替换成你在 3.3 步骤中得到的链接:
const GAS_URL = "PASTE_YOUR_LINK_ENDING_IN_exec_HERE";小组件代码的开头大致是这样的(你只需要替换带有 GAS_URL 的那一行,其余部分不要动):
```dataviewjs
const GAS_URL = "https://script.google.com/macros/s/AKfyc…………/exec";
const obsidian = require("obsidian");
const fileName = dv.current().file.name;
const m = fileName.match(/^(\d{2})-(\d{2})-(\d{4})$/);
if (!m) {
dv.paragraph("⚠️ The note title must be in the DD-MM-YYYY format");
} else {
// … then comes the long widget code: week, progress,
// the "Tasks" button, loading events and tasks from the calendar …
}
```
The full widget code (click to expand)
This is the same block that’s in my template. Copy it whole — from the line starting with the word
dataviewjs, and to the closing backticks — into the daily-note template. Replace onlyGAS_URLin the first line with your link from step 3.3.```dataviewjs const GAS_URL = "YOUR_LINK_HERE/exec"; const obsidian = require("obsidian"); const fileName = dv.current().file.name; const m = fileName.match(/^(\d{2})-(\d{2})-(\d{4})$/); if (!m) { dv.paragraph("⚠️ The note title must be in the DD-MM-YYYY format"); } else { const [, dd, mm, yyyy] = m; const date = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd)); const dateISO = `${yyyy}-${mm}-${dd}`; const yesterday = new Date(date); yesterday.setDate(date.getDate() - 1); const tomorrow = new Date(date); tomorrow.setDate(date.getDate() + 1); const fmt = d => `${String(d.getDate()).padStart(2,"0")}-${String(d.getMonth()+1).padStart(2,"0")}-${d.getFullYear()}`; const prevNote = fmt(yesterday); const nextNote = fmt(tomorrow); const dayNames = ["MON","TUE","WED","THU","FRI","SAT","SUN"]; const dow = date.getDay(); const todayIdx = dow === 0 ? 6 : dow - 1; const progress = (((todayIdx + 1) / 7) * 100).toFixed(0); const weekHtml = dayNames.map((label, i) => { let cls = "el-day"; if (i === todayIdx) cls += " is-today"; else if (i < todayIdx) cls += " is-past"; else cls += " is-future"; return `<span class="${cls}">${label}</span>`; }).join(`<span class="el-dot-sep">•</span>`); const CACHE_KEY = `eltonlabs-gcal-${dateISO}`; function readCache() { try { const raw = localStorage.getItem(CACHE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function writeCache(data) { const payload = { ...data, _ts: Date.now() }; try { localStorage.setItem(CACHE_KEY, JSON.stringify(payload)); } catch {} return payload; } const root = dv.el("div", "", { cls: "eltonlabs-wrapper" }); root.innerHTML = ` <style> .el-card { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 12px; overflow: hidden; margin: 8px 0; } .el-accent { height: 3px; background: linear-gradient(90deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); } .el-body { padding: 12px 16px 14px; } .el-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; } .el-title { display:flex; align-items:center; gap:7px; margin:0; font-size:0.92em; font-weight:600; color:var(--text-normal); } .el-dot { width:8px; height:8px; border-radius:50%; background: var(--interactive-accent); box-shadow: 0 0 6px rgba(99,102,241,.5); flex-shrink:0; } .el-actions { display:flex; align-items:center; gap:6px; } .el-icon-btn { width:26px; height:26px; display:flex; align-items:center; justify-content:center; background:transparent; border:none; border-radius:6px; cursor:pointer; color:var(--text-muted); font-size:0.88em; transition: background .15s, color .15s; padding:0; } .el-icon-btn:hover { background: var(--interactive-hover); color: var(--text-normal); } .el-icon-btn.spin { animation: el-spin .55s linear; } .el-toggle-btn { display:flex; align-items:center; gap:5px; padding:4px 10px; border-radius:7px; border:1px solid var(--background-modifier-border); background: var(--interactive-normal); color: var(--text-normal); font-size:0.78em; font-weight:500; cursor:pointer; transition: background .15s, border-color .15s; } .el-toggle-btn:hover { background: var(--interactive-hover); border-color: var(--interactive-accent); } @keyframes el-spin { to { transform: rotate(360deg); } } @keyframes el-shimmer { to { background-position: -200% 0; } } .el-weekrow { text-align:center; font-size:0.95em; margin-bottom:12px; } .el-day { padding: 4px 9px; border-radius:6px; } .el-day.is-today { font-weight:bold; color:#fff; background: linear-gradient(135deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); box-shadow: 0 2px 8px rgba(99,102,241,.3); } .el-day.is-past { color: var(--color-green, #10b981); opacity:.6; text-decoration: line-through; } .el-day.is-future { color: var(--text-muted); opacity:.55; } .el-dot-sep { color: var(--text-faint, var(--text-muted)); margin:0 8px; } .el-progress { background: var(--background-modifier-border); height:4px; border-radius:2px; overflow:hidden; } .el-progress-fill { background: linear-gradient(90deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); height:100%; transition: width .3s; } .el-progress-label { text-align:center; color: var(--text-muted); font-size:0.85em; margin-top:8px; } .el-navlinks { display:flex; justify-content:space-between; margin-top:12px; } .el-navlinks a { display:inline-flex; align-items:center; gap:5px; padding:5px 14px; border-radius:8px; background: var(--interactive-normal); border:1px solid var(--background-modifier-border); color: var(--text-normal) !important; text-decoration:none !important; font-size:13px; cursor:pointer; transition: background .15s; } .el-navlinks a:hover { background: var(--interactive-hover); } .el-meta { font-size:0.71em; color: var(--text-faint, var(--text-muted)); margin-bottom:12px; } .el-section { font-size:0.69em; color: var(--text-muted); text-transform:uppercase; letter-spacing:.07em; margin:10px 0 3px; padding-left:2px; } .el-divider { border:none; border-top:1px solid var(--background-modifier-border); margin:8px 0; } .el-item { display:flex; gap:8px; align-items:center; padding:4px 6px; border-radius:6px; font-size:0.875em; transition: background .12s; } .el-item.clickable { cursor:pointer; } .el-item.clickable:hover { background: var(--interactive-hover); } .el-item.completed { opacity:.38; } .el-item.completed .el-item-title { text-decoration: line-through; } .el-item.pending { opacity:.5; pointer-events:none; } .el-time { color: var(--text-accent, var(--interactive-accent)); font-variant-numeric: tabular-nums; min-width:60px; font-size:.85em; flex-shrink:0; } .el-checkbox { min-width:20px; text-align:center; flex-shrink:0; } .el-item-title { flex:1; } .el-spinner { display:inline-block; width:13px; height:13px; border:2px solid var(--background-modifier-border); border-top-color: var(--interactive-accent); border-radius:50%; animation: el-spin .65s linear infinite; vertical-align:middle; } .el-empty { color: var(--text-muted); font-size:.85em; padding:2px 2px 8px; font-style:italic; } .el-skeleton { height:16px; border-radius:6px; background: linear-gradient(90deg, var(--background-modifier-border) 25%, var(--background-primary) 50%, var(--background-modifier-border) 75%); background-size:200% 100%; animation: el-shimmer 1.2s ease infinite; margin:8px 0; } .el-skeleton.w80 { width:80%; } .el-skeleton.w55 { width:55%; } .el-load-btn { display:flex; align-items:center; justify-content:center; gap:7px; width:100%; padding:7px 14px; border-radius:8px; border:1px dashed var(--background-modifier-border); cursor:pointer; background:transparent; color: var(--text-muted); font-size:.85em; transition: background .15s, border-color .15s, color .15s; margin-top:2px; } .el-load-btn:hover { background: var(--interactive-hover); border-color: var(--interactive-accent); color: var(--text-normal); } .el-cmdbar-wrap { margin-top:14px; } .el-cmdbar { display:flex; align-items:stretch; height:38px; border:1px solid var(--background-modifier-border); border-radius:10px; overflow:hidden; background: var(--background-primary); box-sizing:border-box; transition: border-color .2s; } .el-cmdbar:focus-within { border-color: var(--interactive-accent); box-shadow: 0 0 0 2px rgba(99,102,241,.18); } .el-cmdbar-icon { display:flex; align-items:center; padding:0 2px 0 10px; color: var(--text-faint, var(--text-muted)); font-size:.78em; flex-shrink:0; line-height:1; user-select:none; pointer-events:none; } .el-cmdbar input[type=text] { flex:1; min-width:0; width:0; height:100%; background:transparent; border:none; outline:none !important; box-shadow:none !important; padding:0 8px 0 4px; margin:0; color: var(--text-normal); font-size:.86em; box-sizing:border-box; appearance:none; -webkit-appearance:none; } .el-cmdbar input[type=text]::placeholder { color: var(--text-faint, var(--text-muted)); } .el-vbar { width:1px; margin:7px 0; flex-shrink:0; background: var(--background-modifier-border); align-self:stretch; } .el-cmdbar input[type=date] { height:100%; width:120px; flex-shrink:0; background:transparent; border:none; outline:none !important; box-shadow:none !important; padding:0 8px; margin:0; color: var(--text-muted); font-size:.8em; color-scheme: dark light; cursor:pointer; box-sizing:border-box; appearance:none; -webkit-appearance:none; } .el-cta-btn { flex-shrink:0; height:100% !important; border-radius:0 !important; border-left:1px solid rgba(99,102,241,.3) !important; border-top:none !important; border-right:none !important; border-bottom:none !important; padding:0 16px !important; margin:0 !important; font-size:.84em !important; font-weight:600 !important; white-space:nowrap !important; cursor:pointer !important; display:flex !important; align-items:center !important; gap:5px !important; box-sizing:border-box !important; outline:none !important; transition: filter .18s !important; } .el-cta-btn:hover:not(:disabled) { filter: brightness(1.1) !important; } .el-cta-btn:disabled { opacity:.45 !important; cursor:default !important; filter:none !important; } .el-btn-spinner { display:inline-block; width:12px; height:12px; border:2px solid rgba(255,255,255,.35); border-top-color: currentColor; border-radius:50%; animation: el-spin .6s linear infinite; } </style> <div class="el-card" id="el-card"> <div class="el-accent"></div> <div class="el-body"> <div class="el-head"> <h4 class="el-title"><span class="el-dot"></span><span id="el-title-text">Week</span></h4> <div class="el-actions"> <button class="el-icon-btn" id="el-refresh" title="Refresh" style="display:none">🔄</button> <button class="el-toggle-btn" id="el-toggle">🗓️ Tasks</button> </div> </div> <div id="el-view-nav"> <div class="el-weekrow">${weekHtml}</div> <div class="el-progress"><div class="el-progress-fill" style="width:${progress}%"></div></div> <div class="el-progress-label">Week ${progress}% complete</div> <div class="el-navlinks"> <a data-link="${prevNote}">← Yesterday</a> <a data-link="${nextNote}">Tomorrow →</a> </div> </div> <div id="el-view-cal" style="display:none"> <div id="el-cal-body"> <div class="el-skeleton"></div> <div class="el-skeleton w80"></div> <div class="el-skeleton w55"></div> </div> </div> </div> </div>`; const navView = root.querySelector("#el-view-nav"); const calView = root.querySelector("#el-view-cal"); const calBody = root.querySelector("#el-cal-body"); const toggleBtn = root.querySelector("#el-toggle"); const refreshBtn = root.querySelector("#el-refresh"); const titleText = root.querySelector("#el-title-text"); let view = "nav"; toggleBtn.addEventListener("click", () => { if (view === "nav") { navView.style.display = "none"; calView.style.display = ""; titleText.textContent = "Google Calendar"; toggleBtn.innerHTML = "📅 Week"; refreshBtn.style.display = ""; view = "cal"; } else { calView.style.display = "none"; navView.style.display = ""; titleText.textContent = "Week"; toggleBtn.innerHTML = "🗓️ Tasks"; refreshBtn.style.display = "none"; view = "nav"; } }); root.querySelectorAll("a[data-link]").forEach(btn => { btn.addEventListener("click", (e) => { e.preventDefault(); app.workspace.openLinkText(btn.dataset.link, "", false); }); }); let isLoading = false; function fmtTime(ts) { const n = ts ? new Date(ts) : new Date(); return `${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`; } function cmdBarHTML() { return ` <div class="el-cmdbar-wrap"> <div class="el-cmdbar"> <span class="el-cmdbar-icon">✏️</span> <input type="text" id="el-input" placeholder="New task…" /> <div class="el-vbar"></div> <input type="date" id="el-date" value="${dateISO}" /> <button class="mod-cta el-cta-btn" id="el-add-btn"><span>+</span> Add</button> </div> </div>`; } function renderCalSkeleton() { calBody.innerHTML = `<div class="el-skeleton"></div><div class="el-skeleton w80"></div><div class="el-skeleton w55"></div>`; } function renderCal(data) { const ev = data.events || []; const tk = data.tasks || []; let html = `<div class="el-meta">updated at ${fmtTime(data._ts)}</div>`; if (!ev.length && !tk.length) { html += `<div class="el-empty">Nothing planned for this day</div>`; } else { if (ev.length) { html += `<div class="el-section">Events</div>`; ev.forEach(e => { html += `<div class="el-item"><span class="el-time">${e.allDay ? "all day" : e.time}</span><span class="el-item-title">📌 ${e.title}</span></div>`; }); } if (tk.length) { const overdueTasks = tk.filter(t => t.overdue); const normalTasks = tk.filter(t => !t.overdue); if (overdueTasks.length) { if (ev.length) html += `<hr class="el-divider">`; html += `<div class="el-section" style="color:#ef4444">Overdue</div>`; overdueTasks.forEach(t => { html += `<div class="el-item clickable" data-id="${t.id}" data-list="${t.listId}" data-status="${t.status}"><span class="el-checkbox">⬜</span><span class="el-item-title" style="color:#ef4444">${t.title}</span></div>`; }); } if (normalTasks.length) { if (ev.length || overdueTasks.length) html += `<hr class="el-divider">`; html += `<div class="el-section">Tasks</div>`; normalTasks.forEach(t => { const done = t.status === "completed"; html += `<div class="el-item ${done ? "completed" : "clickable"}" data-id="${t.id}" data-list="${t.listId}" data-status="${t.status}"><span class="el-checkbox">${done ? "✅" : "⬜"}</span><span class="el-item-title">${t.title}</span></div>`; }); } } } html += cmdBarHTML(); calBody.innerHTML = html; bindCalEvents(); } function renderCalError(msg) { calBody.innerHTML = `<div class="el-empty">⚠️ ${msg}</div><button class="el-load-btn" id="el-cal-retry">🔄 Retry</button>`; calBody.querySelector("#el-cal-retry")?.addEventListener("click", loadCal); } function bindCalEvents() { calBody.querySelectorAll(".el-item.clickable[data-id]").forEach(item => { item.addEventListener("click", async () => { const taskId = item.getAttribute("data-id"); const listId = item.getAttribute("data-list"); item.classList.add("pending"); item.querySelector(".el-checkbox").innerHTML = '<span class="el-spinner"></span>'; try { await obsidian.requestUrl({ url: GAS_URL, method: "POST", contentType: "text/plain", body: JSON.stringify({ action: "complete", taskId, listId }) }); new obsidian.Notice("✅ Task done!"); loadCal(); } catch (err) { item.classList.remove("pending"); item.querySelector(".el-checkbox").innerHTML = "⬜"; new obsidian.Notice("⚠️ " + err.message); } }); }); const input = calBody.querySelector("#el-input"); const dateIn = calBody.querySelector("#el-date"); const btn = calBody.querySelector("#el-add-btn"); if (!input || !btn) return; const add = async () => { const title = input.value.trim(); const due = dateIn.value || dateISO; if (!title) { input.focus(); return; } if (!due) { dateIn.focus(); return; } btn.disabled = true; btn.innerHTML = `<span class="el-btn-spinner"></span>`; try { await obsidian.requestUrl({ url: GAS_URL, method: "POST", contentType: "text/plain", body: JSON.stringify({ title, dueDate: `${due}T00:00:00.000Z` }) }); new obsidian.Notice(`✅ "${title}" added`); input.value = ""; if (due === dateISO) { loadCal(); } else { btn.disabled = false; btn.innerHTML = `<span>+</span> Add`; } } catch (err) { btn.disabled = false; btn.innerHTML = `<span>+</span> Add`; input.focus(); new obsidian.Notice("⚠️ " + err.message); } }; btn.addEventListener("click", add); input.addEventListener("keydown", e => { if (e.key === "Enter") add(); }); } async function loadCal() { if (isLoading) return; isLoading = true; renderCalSkeleton(); try { const res = await obsidian.requestUrl({ url: `${GAS_URL}?date=${dateISO}`, method: "GET" }); renderCal(writeCache(res.json)); } catch (err) { renderCalError(err.message); } finally { isLoading = false; } } refreshBtn.addEventListener("click", () => { if (isLoading) return; refreshBtn.classList.add("spin"); setTimeout(() => refreshBtn.classList.remove("spin"), 600); loadCal(); }); const cached = readCache(); if (cached) { renderCal(cached); } else { loadCal(); } } ```
Done! Now in the note you see the tasks for today, overdue tasks are highlighted in red, tasks can be marked as done and new ones added. It’s especially convenient on the phone: opened the widget — and quickly jotted down your tasks.
Checking that it works. Open your link in the browser, adding
?debug=1at the end. If you see JSON with a list of your tasks — the server is working correctly.
For those who don't want to figure it out for ages
Try my ready-made Obsidian template and start systematising your information today
Learn about the templateStep 4. Plans for the day and buttons
The next section of the template is plans for the day. Here I have button labels from the Buttons plugin:
`button-spaced-repetition` `button-sport` `button-morning-routine` `button-work-on-project` `button-litso`
Each such label is one button that I press during the day:
| Button | What it launches |
|---|---|
| 🃏 Spaced repetition | Recall the material I’m learning |
| 💪 Workouts | Today’s exercises (change automatically by muscle group) |
| 🙏 Morning | My morning ritual — meditation and concentration |
| 📋 Task | Open the projects and take a task from there (the main button!) |
| 😌 Face | A personal skincare routine |
⚡ What QuickAdd is (what's hiding behind the buttons)
QuickAdd is a plugin that launches pre-configured actions with one command.
In the button’s code, the line
action QuickAdd: …means: “press the button — do this action from QuickAdd.” And the action can be anything — open a specific note, create a new one by template, or run a whole script.Simply put: the button is the “face”, and QuickAdd is the “brain” that decides what happens on the press.
These functions were added gradually. At first the note had only the morning ritual. Then I added spaced repetition, workouts and the rest — as needed, so as not to overload the brain. I advise you too to start with one or two buttons.
Step 5. How the buttons (Buttons) work — in simple words
The most frequent misunderstanding of beginners: “why is `button-sport` written in the note, but a beautiful button appears?” Let’s figure it out.
The logic is divided into two parts: where the button is described and where it’s shown.
Here lie the full descriptions of all the buttons (code + the
^button-… label). This is the "warehouse" of buttons.
Here there's only the short label
`button-sport` — and the plugin substitutes the button from the "warehouse."
Why so complicated? So as not to copy the long button code into every note. Described it once — insert it with a short label as many times as you like.
What a button description looks like
All my buttons are stored in one note MOC - Buttons (or just “Buttons”). Here’s an example of one button’s description:
```button
name 💪 Workouts
type command
action QuickAdd: Open workouts
color black
class btn-inline
```
^button-sport
Let’s go through it line by line:
nameWhat's written on the button (can be with emoji).type commandThe "command" type — the button launches an Obsidian command.actionExactly which command to launch (here — a choice from QuickAdd).color / classThe appearance. btn-inline — a compact inline button.^button-sport⭐ The main thing: the label (block id). This is the button's "address."What ^button-... and the label in the text are
The line ^button-sport right under the code block is a block id (a block anchor) in Obsidian. It gives the button a unique address.
After this, in any note it’s enough to write the label as inline code (in single backticks):
`button-sport`
The Buttons plugin sees this label, finds the needed description by the address ^button-sport and draws a working button in its place. You can put several labels in a row in one line — you’ll get a row of buttons.
The rule is simple:
^button-name(with the “caret”) — this is where the button is described.`button-name`(in quotes) — this is where the button is shown. The name afterbutton-must match.

How to create your own button from scratch
```button … ``` block.name (the label) and the action (which command to launch).^button-water.`button-water` — and the button will appear.Step 6. The main “Task” button: connecting the diary and projects
The ”📋 Task” button is one of the main ones. Its label is button-work-on-project, and it launches the project-to-daily script via QuickAdd.
First, about where the tasks come from. All my projects are in the MOC - Projects note (the 1. Projects folder by the PARA structure). This is a kanban board on the Kanban plugin: just task cards that are dragged from status to status.
The “Task” button does two smart things:
- Pulls tasks from the projects into the diary. A window with your kanban board opens, you tick the needed cards and click “Add” — they appear in the “My plans for the day” section.
- Moves tasks across the board on completion. When you mark a task as done (
- [x]) right in the diary, it automatically moves to the next column of the kanban (e.g. from “In progress” to “Done”) and is removed from the note.
There's also a "turbo mode": if you turn on the "🏁 To the last" toggle (or add the 🏁 icon to the task), on completion it'll go straight to the very last column of the board.
What to replace in the script for yourself
The script is in my 0. Files/4. Templates/Scripts/project-to-daily_01.js. If you take it for yourself, at the very top there are two lines that need to be configured for your system:
const MOC_PATH = "1. Projects/MOC - Projects.md"; // ← the path to YOUR kanban board
const LAST_COLUMN_MARKER = "🏁"; // ← the "straight to the last column" iconMOC_PATH—— 填写你主看板(项目看板)的路径,也就是它的名称以及所在的文件夹。LAST_COLUMN_MARKER—— “极速模式”的标记图标。你可以保留 🏁,也可以换成自己喜欢的任何图标。
还有一点很重要:脚本会根据标题 # Мои планы на день(我的今日计划)来查找插入任务的位置。你的每日笔记中必须有这个标题——否则任务会被添加到笔记末尾。
项目转日记脚本的完整代码(点击展开)
它通过 QuickAdd 作为 User Script 接入一个宏,而这个宏由“任务”按钮调用。
module.exports = async (params) => { const { app, obsidian } = params; const { Modal, Component, Notice } = obsidian; const MOC_PATH = "1. Projects/MOC - Projects.md"; const LAST_COLUMN_MARKER = "🏁"; // 带有此标记的任务会直接进入最后一列 async function parseKanbanBoard(file) { const content = await app.vault.cachedRead(file); const lines = content.split('\n'); const columns = []; let currentColumn = null; let inFrontmatter = false, frontmatterCount = 0, inSettings = false; for (const line of lines) { if (line.trim() === '---') { frontmatterCount++; inFrontmatter = frontmatterCount < 2; continue; } if (inFrontmatter) continue; if (line.includes('')) inSettings = false; continue; } if (line.startsWith('## ')) { currentColumn = { name: line.substring(3).trim(), tasks: [] }; columns.push(currentColumn); continue; } if (currentColumn && line.trim().startsWith('- [ ]')) { const displayText = line.trim().replace(/^- \[ \] - /, '').replace(/^- \[ \] /, '').trim(); currentColumn.tasks.push({ display: displayText, raw: line.trim() }); } } return columns; } function resolveLink(linkText, sourcePath) { const clean = linkText.replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0].trim(); return app.metadataCache.getFirstLinkpathDest(clean, sourcePath); } function isKanban(file) { if (!file) return false; return !!app.metadataCache.getFileCache(file)?.frontmatter?.["kanban-plugin"]; } function tryResolveAsFile(displayText, sourcePath) { const match = displayText.match(/\[\[([^\]]+)\]\]/); if (!match) return null; return resolveLink(`[[${match[1]}]]`, sourcePath); } function cleanText(s) { return (s || '').replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1').trim(); } function waitForMetadataResolved(timeout = 3000) { return new Promise((resolve) => { let ref = null, timer = null; const finish = () => { if (ref) app.metadataCache.offref(ref); if (timer) clearTimeout(timer); resolve(); }; ref = app.metadataCache.on('resolved', finish); timer = setTimeout(finish, timeout); }); } function getAllKanbanBoards() { return app.vault.getMarkdownFiles().filter(f => app.metadataCache.getFileCache(f)?.frontmatter?.["kanban-plugin"] === "board" ); } async function findAndMoveTask(taskText, toLast = false) { const boards = getAllKanbanBoards(); const normalized = cleanText(taskText).toLowerCase(); for (const board of boards) { const content = await app.vault.read(board); const lines = content.split('\n'); let foundIdx = -1; let curColName = '', curColStart = -1; const cols = []; let inFM = false, fmCount = 0, inSet = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.trim() === '---') { fmCount++; inFM = fmCount < 2; continue; } if (inFM) continue; if (line.includes('')) inSet = false; continue; } if (line.startsWith('## ')) { if (curColStart !== -1) cols.push({ name: curColName, start: curColStart }); curColName = line.substring(3).trim(); curColStart = i; continue; } if (line.trim().startsWith('- [ ]') || line.trim().startsWith('- [x]')) { const t = cleanText(line.trim().replace(/^- \[[x ]\] - /, '').replace(/^- \[[x ]\] /, '')).toLowerCase(); if (t === normalized) foundIdx = i; } } if (curColStart !== -1) cols.push({ name: curColName, start: curColStart }); for (let i = 0; i < cols.length; i++) cols[i].end = i + 1 < cols.length ? cols[i + 1].start - 1 : lines.length - 1; if (foundIdx === -1) continue; let curColIdx = cols.findIndex(c => foundIdx >= c.start && foundIdx <= c.end); if (curColIdx === -1) continue; const targetIdx = toLast ? cols.length - 1 : curColIdx + 1; if (targetIdx >= cols.length || targetIdx <= curColIdx) return null; const targetCol = cols[targetIdx]; const taskLine = lines[foundIdx]; lines.splice(foundIdx, 1); for (const c of cols) { if (c.start > foundIdx) c.start--; if (c.end >= foundIdx) c.end--; } let insertPos = cols[targetIdx].start + 1; for (let i = cols[targetIdx].start + 1; i <= cols[targetIdx].end; i++) { if (lines[i]?.trim().startsWith('- [ ]')) insertPos = i + 1; } lines.splice(insertPos, 0, taskLine); await app.vault.modify(board, lines.join('\n')); return { taskText: cleanText(taskText), column: targetCol.name }; } return null; } async function syncCompletedTasks() { const activeFile = app.workspace.getActiveFile(); if (!activeFile) return { moved: [], linesRemoved: 0 }; const content = await app.vault.read(activeFile); const lines = content.split('\n'); const completed = []; const completedLineIndices = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const m = line.match(/^- \[x\] (.+?)(\s*\|.*)?$/); if (m) { let text = m[1].trim(); const toLast = text.includes(LAST_COLUMN_MARKER); if (toLast) text = text.split(LAST_COLUMN_MARKER).join('').replace(/\s+/g, ' ').trim(); completed.push({ text, toLast }); completedLineIndices.push(i); } } if (completed.length === 0) return { moved: [], linesRemoved: 0 }; const moved = []; const indicesToRemove = []; for (let i = 0; i < completed.length; i++) { const { text, toLast } = completed[i]; const result = await findAndMoveTask(text, toLast); if (result) { moved.push(result); indicesToRemove.push(completedLineIndices[i]); } } if (indicesToRemove.length > 0) { const newLines = lines.filter((_, idx) => !indicesToRemove.includes(idx)); await app.vault.modify(activeFile, newLines.join('\n')); } return { moved, linesRemoved: indicesToRemove.length }; } async function addTasksToNote(taskTexts) { const activeFile = app.workspace.getActiveFile(); if (!activeFile) { new Notice('❌ 没有打开的笔记', 3000); return false; } const content = await app.vault.read(activeFile); const lines = content.split('\n'); let insertAfter = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].includes('# Мои планы на день')) { for (let j = i + 1; j < lines.length; j++) { if (lines[j].match(/^# /)) break; if (lines[j].match(/^- \[[x ]\]/)) insertAfter = j; } if (insertAfter === -1) { for (let j = i + 1; j < lines.length; j++) { if (lines[j].match(/^# /)) break; if (lines[j].includes('button-')) insertAfter = j; } } break; } } if (insertAfter === -1) insertAfter = lines.length - 1; const insertAt = insertAfter + 1; const taskLines = taskTexts.map(text => `- [ ] ${text}`); lines.splice(insertAt, 0, ...taskLines); await app.vault.modify(activeFile, lines.join('\n')); return true; } // 阶段 1 — 同步已完成的任务 await new Promise(async (resolve) => { const result = await syncCompletedTasks(); if (result.moved.length === 0) { new Notice('ℹ️ 没有需要转移的已完成任务', 2500); } else { const summary = result.moved.map(m => `✅ ${m.taskText} → ${m.column}`).join('\n'); new Notice(`已移动任务:${result.moved.length}\n${summary}\n🗑️ 已从笔记中移除:${result.linesRemoved}`, 5000); } if (result.moved.length > 0) await waitForMetadataResolved(); resolve(); }); // 阶段 2 — 从看板中选择任务(可多选) await new Promise((resolve) => { class TaskPickerModal extends Modal { constructor() { super(app); this.comp = new Component(); this.comp.load(); this.history = []; this.selectedCards = new Map(); this.addBtn = null; this.toLastMode = false; } onOpen() { this.modalEl.style.cssText = `width: 860px; max-width: 95vw; height: 80vh; display: flex; flex-direction: column; padding: 0;`; this.contentEl.style.cssText = `display: flex; flex-direction: column; height: 100%; overflow: hidden;`; this.renderBoard(MOC_PATH, 'MOC - Projects', true); } onClose() { this.comp.unload(); this.contentEl.empty(); resolve(); } async renderBoard(filePath, title, isRoot = false) { this.contentEl.empty(); this.selectedCards.clear(); const file = app.vault.getAbstractFileByPath(filePath); if (!file) { this.contentEl.createDiv({ text: `❌ 找不到文件:${filePath}` }); return; } const columns = await parseKanbanBoard(file); const header = this.contentEl.createDiv(); header.style.cssText = `padding: 18px 24px 14px; border-bottom: 1px solid var(--background-modifier-border); flex-shrink: 0;`; header.createDiv({ text: title }).style.cssText = `font-size: 1.2em; font-weight: 600;`; header.createDiv({ text: '✓ 勾选 · 双击进入看板' }).style.cssText = `margin-top: 4px; font-size: 0.8em; color: var(--text-muted);`; const body = this.contentEl.createDiv(); body.style.cssText = `flex: 1; overflow: hidden; display: flex; flex-direction: column;`; this.renderKanbanBoard(body, columns, filePath, isRoot); const footer = this.contentEl.createDiv(); footer.style.cssText = `padding: 12px 20px; border-top: 1px solid var(--background-modifier-border); flex-shrink: 0; display: flex; justify-content: space-between; align-items: center;`; const backBtn = footer.createEl('button', { text: isRoot ? '' : '← 返回' }); backBtn.style.cssText = `padding: 7px 14px; border-radius: 6px; cursor: pointer; font-size: 0.9em; visibility: ${isRoot ? 'hidden' : 'visible'};`; backBtn.onclick = () => { const prev = this.history.pop(); if (prev) this.renderBoard(prev.path, prev.title, prev.isRoot); }; const rightGroup = footer.createDiv(); rightGroup.style.cssText = `display: flex; align-items: center; gap: 10px;`; const toLastBtn = rightGroup.createEl('button', { text: `${LAST_COLUMN_MARKER} 直达最后一列` }); toLastBtn.style.cssText = `padding: 7px 12px; border-radius: 6px; cursor: pointer; font-size: 0.9em; border: 1px solid var(--background-modifier-border); background: var(--background-secondary); color: var(--text-normal); transition: background 0.15s, color 0.15s, border 0.15s;`; const syncToLastBtn = () => { if (this.toLastMode) { toLastBtn.style.background = 'var(--interactive-accent)'; toLastBtn.style.color = 'white'; toLastBtn.style.borderColor = 'var(--interactive-accent)'; } else { toLastBtn.style.background = 'var(--background-secondary)'; toLastBtn.style.color = 'var(--text-normal)'; toLastBtn.style.borderColor = 'var(--background-modifier-border)'; } }; toLastBtn.onclick = () => { this.toLastMode = !this.toLastMode; syncToLastBtn(); }; syncToLastBtn(); this.addBtn = rightGroup.createEl('button', { text: '+ 添加任务 (0)' }); this.addBtn.style.cssText = `padding: 7px 16px; border-radius: 6px; cursor: pointer; font-size: 0.9em; background: var(--interactive-accent); color: white; border: none; opacity: 0.35; pointer-events: none; transition: opacity 0.15s;`; this.addBtn.onclick = async () => { if (this.selectedCards.size === 0) return; const tasks = Array.from(this.selectedCards.values()).map(t => this.toLastMode ? `${t} ${LAST_COLUMN_MARKER}` : t); const ok = await addTasksToNote(tasks); if (ok) { new Notice(`✅ 已添加任务:${tasks.length}`, 3000); this.close(); } }; } updateAddButton() { if (!this.addBtn) return; const count = this.selectedCards.size; this.addBtn.textContent = `+ 添加任务 (${count})`; if (count > 0) { this.addBtn.style.opacity = '1'; this.addBtn.style.pointerEvents = 'auto'; } else { this.addBtn.style.opacity = '0.35'; this.addBtn.style.pointerEvents = 'none'; } } renderKanbanBoard(container, columns, sourcePath, isRoot) { if (columns.length === 0) { container.createDiv({ text: '未找到任何列' }).style.cssText = `text-align:center; padding: 60px; opacity: 0.6;`; return; } const board = container.createDiv(); board.style.cssText = `display: flex; flex-direction: row; gap: 14px; padding: 16px 20px; overflow-x: auto; overflow-y: hidden; height: 100%; align-items: flex-start; box-sizing: border-box;`; columns.forEach(column => { const col = board.createDiv(); col.style.cssText = `min-width: 200px; max-width: 240px; flex-shrink: 0; display: flex; flex-direction: column; background: var(--background-secondary); border-radius: 10px; overflow: hidden; border: 1px solid var(--background-modifier-border); max-height: 100%;`; const colHeader = col.createDiv(); colHeader.style.cssText = `padding: 10px 14px; font-weight: 600; font-size: 0.9em; background: var(--background-modifier-border); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;`; colHeader.createSpan({ text: column.name }); const badge = colHeader.createSpan({ text: String(column.tasks.length) }); badge.style.cssText = `background: var(--interactive-accent); color: white; border-radius: 10px; padding: 1px 7px; font-size: 0.78em; font-weight: 600;`; const taskList = col.createDiv(); taskList.style.cssText = `padding: 8px; overflow-y: auto; flex: 1;`; if (column.tasks.length === 0) { taskList.createDiv({ text: '没有任务' }).style.cssText = `padding: 12px; font-size: 0.82em; color: var(--text-muted); text-align: center; opacity: 0.6;`; } column.tasks.forEach(task => { const linkedFile = tryResolveAsFile(task.display, sourcePath); const canDrill = linkedFile && isKanban(linkedFile); const cleanDisplay = task.display.replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1'); const cardWrapper = taskList.createDiv(); cardWrapper.style.cssText = `display: flex; align-items: flex-start; gap: 6px; margin-bottom: 6px; cursor: pointer;`; const checkbox = cardWrapper.createEl('input', { type: 'checkbox' }); checkbox.style.cssText = `margin-top: 10px; cursor: pointer; flex-shrink: 0;`; const card = cardWrapper.createDiv(); card.style.cssText = `padding: 8px 10px; border-radius: 7px; flex: 1; background: var(--background-primary); border: 1px solid ${canDrill ? 'var(--interactive-accent)' : 'var(--background-modifier-border)'}; font-size: 0.88em; line-height: 1.4; transition: background 0.1s, border 0.1s; user-select: none;`; card.textContent = cleanDisplay; if (canDrill) card.createSpan({ text: ' ↗' }).style.cssText = `opacity: 0.5; font-size: 0.85em;`; card.dataset.canDrill = String(canDrill); const taskId = `${sourcePath}::${task.display}`; const toggleSelection = () => { if (this.selectedCards.has(taskId)) { this.selectedCards.delete(taskId); checkbox.checked = false; card.style.background = 'var(--background-primary)'; card.style.borderColor = canDrill ? 'var(--interactive-accent)' : 'var(--background-modifier-border)'; card.style.color = ''; } else { this.selectedCards.set(taskId, task.display); checkbox.checked = true; card.style.background = 'var(--interactive-accent)'; card.style.color = 'white'; card.style.borderColor = 'var(--interactive-accent)'; } this.updateAddButton(); }; checkbox.addEventListener('change', (e) => { e.stopPropagation(); toggleSelection(); }); cardWrapper.addEventListener('click', (e) => { if (e.target !== checkbox) toggleSelection(); }); card.addEventListener('dblclick', (e) => { e.stopPropagation(); const target = linkedFile || tryResolveAsFile(task.display, sourcePath); if (target && isKanban(target)) { this.history.push({ path: sourcePath, title: app.vault.getAbstractFileByPath(sourcePath)?.basename || sourcePath, isRoot }); this.renderBoard(target.path, target.basename, false); } }); }); }); } } new TaskPickerModal().open(); }); };

第 7 步:自动结转未完成的任务
除了按钮,模板里还有一段代码——它会把前一天未完成的任务结转到今天。它基于 Templater 插件运行:新建笔记时,这段代码执行一次,然后变成一份普通的任务列表。
🛠️ 什么是 Templater
Templater 是一个”智能”模板插件。普通模板只会插入一成不变的文本,而 Templater 模板可以思考:填入今天的日期、星期几,或者像我们这里一样,找出昨天未完成的任务并结转过来。
它只在创建笔记的那一刻运行一次。代码(写在
<%和%>标记之间)执行后就消失了,原地留下一段普通的现成文本。你不需要理解它:把代码粘贴进模板一次就够了,之后一切都会自动完成。
逻辑很简单,也非常方便:
找到当天之前的最后一篇笔记
收集所有未关闭的任务
把它们插入新笔记
下面就是模板里的这段代码(原样粘贴即可,无需修改——如果你愿意,只需改一下日记文件夹的路径):
<%*
const moment = tp.obsidian.moment;
const fn = tp.file.title;
const date = moment(fn, 'DD-MM-YYYY');
// On Sundays we add a backup button
if (date.isValid() && date.day() === 0) {
tR += '- [ ] `button-local-backup`\n';
}
const pathToDailyNotes = "2. Areas/Дневники/Ежедневные заметки"; // ← your path
const currentDate = moment(tp.file.title, 'DD-MM-YYYY');
const today = moment().startOf('day');
const referenceDate = currentDate.isAfter(today) ? today.clone().add(1, 'day') : currentDate;
function findLatestDailyNoteBeforeDate(beforeDate) {
const folder = app.vault.getAbstractFileByPath(pathToDailyNotes);
if (!folder || !folder.children) return null;
const dailyFiles = folder.children
.filter(file => {
if (file.extension !== "md") return false;
if (!file.name.match(/^\d{2}-\d{2}-\d{4}\.md$/)) return false;
const fileDate = moment(file.name.replace('.md', ''), 'DD-MM-YYYY');
return fileDate.isBefore(beforeDate) && fileDate.isSameOrBefore(today);
})
.sort((a, b) => {
const dateA = moment(a.name.replace('.md', ''), 'DD-MM-YYYY');
const dateB = moment(b.name.replace('.md', ''), 'DD-MM-YYYY');
return dateB.valueOf() - dateA.valueOf();
});
return dailyFiles.length > 0 ? dailyFiles[0] : null;
}
if (!currentDate.isValid()) {
tR += `❌ Couldn't determine the date from the file name. Format: DD-MM-YYYY.`;
} else {
const yesterday = referenceDate.clone().subtract(1, 'day').format('DD-MM-YYYY');
let targetFile = app.vault.getAbstractFileByPath(`${pathToDailyNotes}/${yesterday}.md`);
let targetDate = yesterday;
if (!targetFile) {
targetFile = findLatestDailyNoteBeforeDate(referenceDate);
if (targetFile) targetDate = targetFile.name.replace('.md', '');
}
if (!targetFile) {
tR += `❌ No daily notes before ${referenceDate.format('DD-MM-YYYY')}.`;
} else {
const fileContent = await app.vault.read(targetFile);
const tasks = fileContent.split("\n").filter(line => line.trim().startsWith("- [ ]"));
if (tasks.length === 0) {
tR += `✅ No uncompleted tasks for ${targetDate}.`;
} else {
tR += `## 🔁 Uncompleted tasks from ${targetDate} (the last note)\n\n`;
tR += tasks.join("\n");
}
}
}
%>如果你的日记中断了好几天,脚本依然会找到当天之前的 最后一篇笔记,并从中提取任务——不会丢失任何内容。只有当你的文件夹不同时,才需要修改
pathToDailyNotes。
第 8 步:习惯和饮食按钮(Meta Bind)
模板接下来是习惯部分。它基于 Meta Bind 插件运行,这个插件让你可以直接在正文中编辑笔记的属性——不用再滚动到顶部去打开属性面板。
🔗 什么是 Meta Bind
Meta Bind 把笔记的属性(就是第 2 步里的那些)与正文中便捷的元素连接起来:开关、复选框、输入框。
没有它的话,要标记一个习惯,你得滚动到顶部手动修改属性。有了它,你只需在浏览笔记时拨动”🏃 运动”开关,属性里的复选框就会自动勾选。
像
INPUT[toggle:Спорт]这样的写法,意思正是:“在这里放一个开关,并与 Спорт 属性关联。”
`INPUT[toggle:Спорт]` 🏃♂️ Sport
`INPUT[toggle:Чтение]` 📖 Reading
`INPUT[toggle:Прогулка]` 🚶♂️ Walk
这里的 INPUT[toggle:Спорт] 是一个与 Спорт 属性关联的开关。你在笔记正文中拨动开关,属性里的复选框就会自动勾选。而图表正是从属性中获取数据的,所以你的习惯会立刻进入统计。
饮食的设置方式相同,只是用的是文本框:
🥚 **Breakfast:** `INPUT[text:Завтрак]`
🍕 **Lunch:** `INPUT[text:Обед]`
🥧 **Dinner:** `INPUT[text:Ужин]`
🥨 **Snack:** `INPUT[text:Перекусы]`
**Total:** `INPUT[text:Итого_ккал]`
`button-calories`
最后是 button-calories 按钮——”🌮 计算卡路里”。这是我个人的按钮,它会借助 AI,根据我在饮食字段里填写的内容估算卡路里。
不可能每天都填得完美无缺——这很正常。但哪怕每周只填几天,习惯图表和卡路里图表也能呈现出清晰的变化趋势。

第 9 步:想法——最重要的部分
最重要的是——模板最底部的**“想法”**部分。这里没有代码,也没有模板,是一块完全空白、用来思考的地方。
我白天会在这里记下:
- 未来的任务和灵感;
- 让我担心的问题;
- 思考和观察。
如果我现在解决不了某个任务,我不会逼自己。我会在"想法"里写下我 为什么做不了。通常会发现,大脑其实卡在了另一件外部的事情上——而意识到这一点本身,就能消除那种呆滞感。
这些想法之后也继续为我所用:
- 在主页上。 明天,我会在主页(Homepage)的一个专门区块里看到昨天的想法。
- 在 MOC - Дневники 笔记中。 那里有一段代码,会收集所有日记笔记中的想法,并按月份逐日展示。新的任务和原子笔记就从中诞生。
- 在手机上。 我设置了一个小组件,可以把想法直接写入今天笔记的对应部分。在街上突然有灵感时,它必不可少。

代码:按月汇总所有日记笔记中的想法
我把这个代码块粘贴到一篇单独的笔记 MOC - Дневники 中(它位于我的 2. Areas/Дневники)。它会遍历当月所有的日记笔记,从每篇中提取**“想法”**部分(以及属性 Мысль_дня 和 Инсайт_дня,如果存在的话),并以列表形式输出。”← 上一月 / 下一月 →“按钮用来翻看各个月份,”🔽 最新优先”用来切换排序。
需要替换成你自己的内容
代码中有一行是日记文件夹的路径——请换成你自己的路径:
dv.pages('"2. Areas/Дневники/Ежедневные заметки"')此外,该代码块会在笔记中查找标题
## Мысли(想法)和属性Дата(日期)。如果你的部分名称不同(例如# Мысли),依然可以正常工作(它会匹配 1 到 3 个 #)。Дата属性必须位于日记笔记的属性中,并且是日期格式。
请完整复制整个代码块——从带有 dataviewjs 的那一行,一直到结尾的反引号:
```dataviewjs
let currentDate = new Date();
let sortOrder = "desc";
const container = dv.el("div", "");
// Control panel
const controls = dv.el("div", "");
controls.style.cssText = `display: flex; align-items: center; gap: 8px; margin: 12px 0 20px 0; flex-wrap: wrap;`;
// Style for the buttons
function styleBtn(btn) {
Object.assign(btn.style, {
display: "inline-flex", "align-items": "center", "justify-content": "center",
gap: "6px", padding: "8px 16px", "border-radius": "8px",
border: "1px solid var(--background-modifier-border)",
background: "var(--background-secondary)", color: "var(--text-normal)",
cursor: "pointer", "font-size": "13px", "font-family": "inherit", "line-height": "1",
transition: "background 0.15s, transform 0.1s, box-shadow 0.15s"
});
btn.onmouseenter = () => { btn.style.background = "var(--background-modifier-hover)"; btn.style.transform = "translateY(-1px)"; };
btn.onmouseleave = () => { btn.style.background = "var(--background-secondary)"; btn.style.transform = "translateY(0)"; };
}
const spacer = dv.el("div", "");
spacer.style.cssText = "flex: 1;";
const prevBtn = dv.el("button", "⬅️ Previous");
const nextBtn = dv.el("button", "Next ➡️");
const sortBtn = dv.el("button", "🔽 Newest first");
styleBtn(prevBtn); styleBtn(nextBtn); styleBtn(sortBtn);
controls.appendChild(prevBtn);
controls.appendChild(nextBtn);
controls.appendChild(spacer);
controls.appendChild(sortBtn);
container.appendChild(controls);
const contentDiv = dv.el("div", "");
container.appendChild(contentDiv);
async function renderMonth(date) {
contentDiv.innerHTML = "";
const start = new Date(date.getFullYear(), date.getMonth(), 1);
const end = new Date(date.getFullYear(), date.getMonth() + 1, 0);
const title = dv.el("h2", `${start.toLocaleDateString("en", { month: "long", year: "numeric" })}`);
title.style.cssText = "margin: 0 0 12px 0; font-size: 1.1em;";
contentDiv.appendChild(title);
// ↓↓↓ REPLACE the path with your own daily-notes folder ↓↓↓
const dailyNotes = dv.pages('"2. Areas/Дневники/Ежедневные заметки"')
.where(p => {
if (!p.Дата) return false;
const d = new Date(p.Дата);
return d >= start && d <= end;
})
.sort(p => p.Дата, sortOrder);
for (const note of dailyNotes) {
const content = await dv.io.load(note.file.path);
const match = content.match(/#{1,3}\s*Мысли\s*\n([\s\S]*?)(?=\n#{1,3}\s|$)/i);
const thoughtOfDay = note["Мысль_дня"];
const insightOfDay = note["Инсайт_дня"];
if (!match && !thoughtOfDay && !insightOfDay) continue;
const block = dv.el("div", "");
block.style.cssText = "border-bottom: 1px solid var(--background-modifier-border); padding: 8px 0;";
block.appendChild(dv.el("h4", note.file.name));
if (thoughtOfDay) block.appendChild(dv.el("p", `💭 Thought of the day: ${thoughtOfDay}`));
if (insightOfDay) block.appendChild(dv.el("p", `💡 Insight: ${insightOfDay}`));
if (match) block.appendChild(dv.el("p", `📝 Thoughts:\n${match[1].trim()}`));
contentDiv.appendChild(block);
}
}
prevBtn.onclick = () => { currentDate.setMonth(currentDate.getMonth() - 1); renderMonth(currentDate); };
nextBtn.onclick = () => { currentDate.setMonth(currentDate.getMonth() + 1); renderMonth(currentDate); };
sortBtn.onclick = () => {
if (sortOrder === "desc") { sortOrder = "asc"; sortBtn.textContent = "🔼 Oldest first"; }
else { sortOrder = "desc"; sortBtn.textContent = "🔽 Newest first"; }
renderMonth(currentDate);
};
await renderMonth(currentDate);
```
(insert an image here)
The home page: habits and thoughts
Every morning Obsidian opens for me not on an empty spot, but on a home page (the MOC - HOME note, the Homepage plugin). On it I immediately see the two most important things: the habits chart for the current month and yesterday’s thoughts. So the day starts with a short “overview of myself.”
🏠 What Homepage is
Homepage is a plugin that makes a chosen note the “home” one: it’s precisely the one that opens on launching Obsidian (and via the home button).
It’s convenient to make a dashboard the home page — a note with charts, buttons and a summary. Then every launch of the program starts with an overview, rather than an empty screen.
This block consists of two tabs — “Habits” and “Thoughts”. The “Habits” tab builds a completion chart for the month (it takes the data from the daily notes’ properties). The “Thoughts” tab shows the “Thoughts” section from yesterday’s note.
What to replace for yourself
At the start of the code there’s one line with the path to the daily-notes folder — specify your path:
const HABITS_FOLDER = "2. Areas/Дневники/Ежедневные заметки";And in the
habitsarray, list your habits — the names must exactly match the property names in the template (Спорт,Чтение,Прогулка, etc.).
For the habits chart you need the Obsidian Charts plugin (it provides the
window.renderChartfunction). Install it the same way as the rest: Browse → "Charts" → Install and enable.
Copy the block whole — from the line with the word dataviewjs and to the closing backticks:
```dataviewjs
// ⚙️ SETTINGS — replace the paths and the habit list for yourself
const { MarkdownRenderer } = require('obsidian');
const HABITS_FOLDER = "2. Areas/Дневники/Ежедневные заметки"; // ← your daily-notes folder
const habits = [
{ name: "Sport", key: "Спорт" },
{ name: "Reading", key: "Чтение" },
{ name: "Walk", key: "Прогулка" }
];
// 📅 Current month
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const pad = n => String(n).padStart(2, "0");
const monthStr = pad(month);
const totalDays = new Date(year, month, 0).getDate();
const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const monthName = MONTH_NAMES[month - 1];
// 📅 Yesterday's date (for the "Thoughts" section)
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const yFileName = `${pad(yesterday.getDate())}-${pad(yesterday.getMonth() + 1)}-${yesterday.getFullYear()}`;
const yFilePath = `${HABITS_FOLDER}/${yFileName}.md`;
// 🛠️ Helpers
const asBool = v => v === true || v === "true" || v === 1;
function gradientColor(pct) {
let r, g, b = 0;
if (pct < 50) { r = 255; g = Math.round(255 * pct / 50); }
else { g = 255; r = Math.round(255 - 255 * (pct - 50) / 50); }
return `rgb(${r},${g},${b})`;
}
// 🚀 Render
let viewMode = "habits";
async function render() {
this.container.innerHTML = "";
const tabs = this.container.createEl("div");
tabs.style.cssText = "display:flex; gap:8px; margin-bottom:14px; flex-wrap:wrap;";
const makeTab = (label, mode) => {
const active = viewMode === mode;
const btn = tabs.createEl("button", { text: label });
btn.style.cssText = "padding:6px 16px; border-radius:6px; cursor:pointer; border:1px solid var(--interactive-accent); " +
(active ? "background:var(--interactive-accent); color:var(--text-on-accent); font-weight:600;"
: "background:transparent; color:var(--text-normal);");
btn.addEventListener("click", () => { if (viewMode !== mode) { viewMode = mode; render.call(this); } });
};
makeTab("Habits", "habits");
makeTab("Thoughts", "thoughts");
const box = this.container.createEl("div");
// ── Habits ──────────────────────────────────────────────
if (viewMode === "habits") {
let pages = dv.pages(`"${HABITS_FOLDER}"`).where(p => {
if (p.date) { const d = dv.date(p.date); return d && d.year === year && d.month === month; }
const n = p.file.name;
return n.includes(`-${monthStr}-${year}`) || n.includes(`${year}-${monthStr}-`);
});
if (pages.length === 0) { box.createEl("p", { text: `No data for ${monthStr}.${year}.` }); return; }
const counts = habits.map(h => pages.filter(p => asBool(p[h.key])).length);
const percents = counts.map(c => Math.round((c / totalDays) * 100));
window.renderChart({
type: "bar",
data: {
labels: habits.map(h => h.name),
datasets: [{
label: `Completion for ${monthStr}.${year} (of ${totalDays} days)`,
data: percents,
backgroundColor: percents.map(p => gradientColor(p)),
borderRadius: 4,
}]
},
options: {
indexAxis: "y",
scales: {
x: { min: 0, max: 100, ticks: { callback: v => v + "%" }, title: { display: true, text: "Percent" } },
y: { title: { display: true, text: "Habits" } }
},
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: ctx => `${ctx.raw}% (${counts[ctx.dataIndex]} of ${totalDays} days)` } },
title: { display: true, text: `Habits — ${monthName} ${year}` }
}
}
}, box);
// ── Thoughts (from yesterday's note) ─────────────────────
} else if (viewMode === "thoughts") {
const tfile = app.vault.getAbstractFileByPath(yFilePath);
box.createEl("div", { attr: { style: "font-weight:600; margin-bottom:10px; color:var(--text-muted);" } })
.setText(`Thoughts — yesterday (${yFileName})`);
if (!tfile) { box.createEl("p", { text: `Note not found: ${yFileName}`, attr: { style: "color:var(--text-muted);" } }); return; }
const content = await app.vault.read(tfile);
const match = content.match(/^#{1,6}\s*Мысли\s*$/m);
if (!match) {
box.createEl("p", { text: "The 'Thoughts' section wasn't found in the note.", attr: { style: "color:var(--text-muted);" } });
} else {
const startIdx = content.indexOf(match[0]) + match[0].length;
const afterText = content.slice(startIdx);
const nextHeading = afterText.match(/\n#{1,6}\s/);
const thoughtsRaw = nextHeading ? afterText.slice(0, nextHeading.index).trim() : afterText.trim();
if (thoughtsRaw) {
const card = box.createEl("div", { attr: { style: "background:var(--background-secondary); border-left:3px solid var(--interactive-accent); border-radius:6px; padding:12px 14px; line-height:1.75; max-height:320px; overflow-y:auto;" } });
await MarkdownRenderer.render(app, thoughtsRaw, card, yFilePath, this.component);
} else {
box.createEl("p", { text: "The 'Thoughts' section is empty.", attr: { style: "color:var(--text-muted);" } });
}
}
const linkWrap = box.createEl("div", { attr: { style: "margin-top:10px;" } });
linkWrap.createEl("a", {
text: `Open ${yFileName} — Thoughts`, cls: "internal-link",
attr: { href: `${yFileName}#Мысли`, "data-href": `${yFileName}#Мысли`, style: "font-size:0.85em; color:var(--interactive-accent); text-decoration:none;" }
});
}
}
render.call(this);
```
(在此插入图片)
纸质日记也在系统里
我并没有完全放弃纸张——有时手写的感觉很不错。我的系统里有一个专门的按钮——”📸 照片转文字”(标签 button-image-2-text,脚本 image-to-text)。
逻辑很简单:我给纸质笔记本的一页拍照 → 按钮借助 AI 识别手写文字 → 我可以把它复制到日记笔记里,或者用它新建一篇笔记。
我还喜欢手绘示意图——任何这样的手绘都可以轻松插入笔记(通过 Excalidraw 插件,或者同样的绘图按钮)。所以在 Obsidian 里写日记功能上没有任何限制:纸张、手绘、语音、照片——一切都汇入同一个地方。

小结
这就是整套系统。看起来规模不小,其实都是用简单的积木搭起来的:
最重要的建议: 不要一次性把所有东西都搭起来。 先启用日记功能,再加一个晨间仪式按钮。等它成为习惯后,再加上任务顺延,然后是日历,最后是项目。系统应该是给大脑减负,而不是增加负担。
在接下来的内容里,我会展示我是怎么给自己做了一个读书插件(我会划出片段,并用它们创建笔记),以及我如何在阅读过程中直接用 AI 来弄清楚不理解的信息。
© 2026 Elton Labs. All rights reserved.