缘起
我之前一直犹豫是否要用Ob,犹豫关键点是Ob没有精细的块、没有notion类的数据表格。
我曾经试过用Obs管理10万个文件,虽然启动时有点卡,但其它时候都很流畅。
由此我突然想到,Ob性能这么好,为什么要一个任务记在一行里?
为什么不能一个任务就写成一个页面呢?
一个任务记一页,搞10年,任务页面也不会超过2万条吧?
大家用Ob做任务管理,一般都用Tasks插件。
这个插件非常好,但因为是一行一个任务,所以导致很多任务属性无法写入。
更加无法很好的管理子任务。
但如果一个任务一个页面,每个页面上加上丰富的属性,
然后就可以配合 Dataview 做出很复杂的查询了。
我理想中的任务管理软件是这样的,比如:
(1)能添加多层级的子任务,并以多层级列表的方式显示指定范围内的任务。
(2)手机或电脑上,能非常方便的添加新任务,特别是在指定任务下添加新的子任务。
(3)快速变更任务的状态。快速的搜索任务库内已有的任务。
(4)丰富多样的、不同角度的数据表格视图。
(5)数据统计。比如我想知道 A任务 下所有已完成的子任务,总共花了多长时间、最近一次完成的是哪个子任务?A任务下所有未完成的子任务,按重要性排?等等。
基本思路
每一条任务一个页面。
页面根据需要、用Templater配置笔记属性。
笔记属性就是任务的各种元数据信息,例如:
---
开始时间:
结束时间:
任务状态:
持续时间:
下级子任务所费时间:
总用时间:
重要性:
紧急性:
任务标签:
截止时间:
入库时间: <% tp.file.creation_date("YYYY-MM-DD HH:mm") %>
---
然用利用 Templater 和 QuickAdd 插件,快速输入新任务。
再利用 Dataview查出自己想找到的任务。
已完成的工作
目前我利用 Cursor,做一个 QuickAdd 宏,完成了一小步。即:
【1】输入新任务时,用Quickadd 打开宏。
【2】下拉框显示任务文件夹中的所有文件夹,以及满足条件的文件(文件也就是页面,条件是页面的「任务状态」属性的值不是「已完成」)。
【3】你选择一个文件夹,或者文件。
当你选择一个文件夹时,新任务会放到这个文件夹下。
这时候要给文件夹新建一个同名文件,因为文件夹本身是一个有子任务的任务,所以它要有页面的笔记属性。
如果该文件夹下已经有同名文件,则不用新建。
这是把文件夹当作新任务的父任务。
当你选择一个文件时,Ob会在文件位置新建一个与文件同名的文件夹,并将选中的文件移动到这个文件夹下。
新任务就放到这个文件的同名文件夹下,以作为这个任务的子任务。
具体脚本见文尾。
待完成的工作
【1】我想在上述「下级子任务所费时间」笔记属性中,自动填入此任务所有已完成的子任务(不包括子任务的子任务)所花费的时间。
假如「任务A」有3个已经完成的子任务,这3个已经完成的子任务所用的总时间,要自动填入「下级子任务所费时间」属性中。
同时,虽然理论上「任务A」的所有子任务时间的和就是「任务A」的时间,但在任务计划没有思考清楚或者其它情况下,「任务A」本身也会花费一定时间,这个时间就手动填入「持续时间」属性中。
然后把该任务所有子任务的「下级子任务所费时间」加起来,再加上该任务手动填入的「持续时间」,加到的和值,自动填入该「总用时间」属性中。
所以,统计当前任务的子任务所用的时间,是统计所有子任务的「总用时间」属性值的和。
这里都只是统计子任务,不统计子任务的子任务(即后代任务),每个层级都只统计子任务的话,会一级级传导上去。
这一步完成以后,我就能知道每个步骤我已经花费了多长时间。
【2】以层级列表的方式显示任务
Tasks最大的问题,在我看来,是子任务管理非常不方便。
Obs虽然有层级清楚的文件夹管理体系,但任务多了以后,每个文件夹下方会有很多已完成的任务杂在其中,且文件夹体系中,无法筛选显示满足某些条件的子任务。
所以, 这一步我希望能在Ob中显示 某个文件夹 下方所有满足条件的子任务和后代任务。 比如显示的任务的笔记属性「任务状态」要是未完成。
补充
以上是一些基本需求,其它的,重复任务之类的,一旦以页面的形式来管理任务,利用好 JS,就都会变得很简单。
/******************************************************************
* task‑inbox.js —— QuickAdd / Templater 通用(onChooseItem 修正版)
******************************************************************/
module.exports = async (params, tp = null) => {
const { app, obsidian } = params;
const { TFolder, TFile, FuzzySuggestModal, Notice } = obsidian;
const ROOT = "任务管理";
const TEMPLATE_MD = "Files/模板库/任务.md";
const DONE_STATUS = new Set(["[[已完成]]"]);
const fm = f => app.metadataCache.getFileCache(f)?.frontmatter || {};
const rel = p => p.startsWith(ROOT + "/") ? p.slice(ROOT.length + 1) : p;
const hasSameNamedFolder = f =>
app.vault.getAbstractFileByPath(`${f.parent.path}/${f.basename}`) instanceof TFolder;
const allow = f =>
f instanceof TFile &&
f.extension === "md" &&
![...DONE_STATUS].some(s => (fm(f)["任务状态"] || "").includes(s)) &&
!hasSameNamedFolder(f) &&
f.parent?.name !== f.basename;
const walk = folder => folder?.children?.flatMap?.(c => {
if (!c) return [];
if (c instanceof TFolder) return c.name.includes("@") ? [] : [c, ...walk(c)];
return allow(c) ? [c] : [];
}) || [];
const sanitize = s => s.replace(/[\/\\:\*\?"<>\|]/g, "_").trim();
const createTask = async (dir, title) => {
const safe = sanitize(title);
const tgt = `${dir}/${safe}.md`;
if (app.vault.getAbstractFileByPath(tgt)) { new Notice("已存在同名任务"); return; }
const tplFile = app.vault.getAbstractFileByPath(TEMPLATE_MD);
const tpl = tplFile ? await app.vault.read(tplFile) : "---\ntitle: \n---\n";
await app.vault.create(tgt, tpl.replace(/title:\s*.*/i, `title: ${safe}`));
await app.workspace.getLeaf(false).openFile(app.vault.getAbstractFileByPath(tgt));
};
const moveIntoFolder = async file => {
if (!file?.path) return null;
const dst = `${file.parent.path}/${file.basename}`;
if (!app.vault.getAbstractFileByPath(dst)) await app.vault.createFolder(dst);
await app.fileManager.renameFile(file, `${dst}/${file.basename}.md`);
return dst;
};
const title = await (
params.quickAddApi?.inputPrompt?.("新任务标题")
?? tp?.system.prompt?.("新任务标题")
);
if (!title) return;
class Picker extends FuzzySuggestModal {
constructor(dir) { super(app); this.dir = dir; }
getItems() { return walk(this.dir); }
getItemText(i) { return `${i instanceof TFolder ? "📁" : "📝"} ${rel(i.path)}`; }
async onChooseItem(it, evt) { /* ← 修正名称 */
const enterChild = it instanceof TFolder &&
((evt && evt.detail === 2) || (!evt && window.event?.ctrlKey));
if (enterChild) { this.close(); setTimeout(() => new Picker(it).open(), 0); return; }
if (it instanceof TFolder) {
await createTask(it.path, title); /* 【3】 */
} else {
const dir = await moveIntoFolder(it); /* 【4】 */
if (dir) await createTask(dir, title);
}
this.close();
}
}
new Picker(app.vault.getAbstractFileByPath(ROOT)).open();
};
module.exports.task_inbox = module.exports;
我是编程小白,用 Cursor 编程只能解决很简单的问题。
希望有大佬能帮助实现这个功能,相信有很多人会有需要。