Obsidian 针对项目文件的自动任务甘特图

Obsidian 针对项目文件的自动任务甘特图

非常开心实现了很久以来 obsidian 中做我理想中的项目管理:一个文件就是一个项目的记录。里面可以划分成约三层的粒度:

整个项目 - 分任务 - 细分事项。其中的细分事项就是这里的最小单位 - 任务。我认为这是非常合适的粒度,并且配合甘特图的 section 达到很好的任务展示效果:

效果

说明

  • 插入该模板到任意文件,即可自动搜集该文件的任务,并生成如上图的甘特图。
  • 使用 Dataview 的时间格式来对各个任务确定 start/due/duration/priority/milestone 等。格式如下图例中源码所示。即每个 key 后面有个空格:
  • 亲本任务如 section1 section2,定义为下面有子任务的任务,这种亲本任务会被自动用作甘特图中的 section 部分,不算做任务。从而达到将大项目划成任务。任务再划成事物。实现项目 - 任务 - 事务的划分层级。
  • 每个子任务除了第一个使用 start 来定义起点以外,其他的都可以使用 [duration:: 1d] 这种格式来自动接在后面。从而实现依赖关系。
  • 如果不想要这种依赖关系,就在这一行子任务中重新定义 start 即可。
  • 使用 [milestone:: yes] 来给甘特图绘制里程碑
  • 使用 [priority:: high/medium/low] 来定义优先级。其中定义为 high 的,在甘特图上会用红色方框表示重要。

其他

代码附上

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```');
}
1 个赞