Obsidian 针对项目文件的自动任务甘特图
非常开心实现了很久以来 obsidian 中做我理想中的项目管理:一个文件就是一个项目的记录。里面可以划分成约三层的粒度:
整个项目 - 分任务 - 细分事项。其中的细分事项就是这里的最小单位 - 任务。我认为这是非常合适的粒度,并且配合甘特图的 section 达到很好的任务展示效果:
效果
说明
- 插入该模板到任意文件,即可自动搜集该文件的任务,并生成如上图的甘特图。
- 使用 Dataview 的时间格式来对各个任务确定 start/due/duration/priority/milestone 等。格式如下图例中源码所示。即每个 key 后面有个空格:
- 亲本任务如 section1 section2,定义为下面有子任务的任务,这种亲本任务会被自动用作甘特图中的 section 部分,不算做任务。从而达到将大项目划成任务。任务再划成事物。实现项目 - 任务 - 事务的划分层级。
- 每个子任务除了第一个使用 start 来定义起点以外,其他的都可以使用
[duration:: 1d]
这种格式来自动接在后面。从而实现依赖关系。 - 如果不想要这种依赖关系,就在这一行子任务中重新定义 start 即可。
- 使用
[milestone:: yes]
来给甘特图绘制里程碑 - 使用
[priority:: high/medium/low]
来定义优先级。其中定义为 high 的,在甘特图上会用红色方框表示重要。
其他
- 我用的是 dataview 的格式来记录任务,如果使用 task 格式,可以自己更改关键词。
- 参考了这篇帖子的效果:Automatic Gantt Chart from Obsidian Tasks & Dataview - Share & showcase - Obsidian Forum
- 使用 components 插件实现所有项目的数据库管理,对每个项目里面又可以进行单独的任务管理。已经实现了我心中的理想场景,很开心!
代码附上
function textParser(taskText) {
let startDate = taskText.match(/\[start::\s*(\d{4}-\d{2}-\d{2})\]/);
let dueDate = taskText.match(/\[due::\s*(\d{4}-\d{2}-\d{2})\]/);
let duration = taskText.match(/\[duration::\s*([\d.]+)\s*(m|h|d|y)\]/i);
let priority = taskText.match(/\[priority::\s*(high|medium|low)\]/i);
let milestone = taskText.includes('[milestone:: yes]');
return {
name: taskText.split('[')[0].trim(),
start: startDate ? startDate[1] : '',
due: dueDate ? dueDate[1] : '',
duration: duration ? `${duration[1]}${duration[2]}` : '',
priority: priority ? priority[1].toLowerCase() : '',
milestone: milestone
};
}
function loopGantt(page) {
if (!page || !page.file || !page.file.tasks || page.file.tasks.length === 0) {
return "";
}
let querySections = "";
let taskArray = page.file.tasks;
let hasParentTask = taskArray.some(task => task.children && task.children.length > 0);
for (let task of taskArray) {
let taskObj = textParser(task.text);
if (hasParentTask && task.children && task.children.length > 0) {
querySections += `section ${taskObj.name}\n`;
continue;
}
let status = task.status === "x" ? "done" : "active";
let critStat = taskObj.priority === "high" ? "crit, " : "";
let mile = taskObj.milestone ? "milestone, " : "";
let taskLine = `${taskObj.name} :${mile}${critStat}${status}, `;
if (taskObj.start) {
taskLine += `${taskObj.start}, `;
}
if (taskObj.due) {
taskLine += `${taskObj.due}`;
} else if (taskObj.duration) {
taskLine += `${taskObj.duration}`;
}
taskLine += '\n';
querySections += taskLine;
}
return querySections;
}
let currentPage = dv.current();
let result = loopGantt(currentPage);
if (result) {
const Mermaid = `gantt
title ${currentPage.file.name}
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
excludes weekends
tickInterval 1day
`;
const fullMermaidCode = Mermaid + result;
dv.paragraph('```mermaid\n' + fullMermaidCode + '\n```');
}