【TP脚本】快速切换任务完成状态并添加日期

演示

250416_快速切换任务完成状态并添加日期-img-250416_133047

说明

使用默认的「切换待办事项」命令时,无法配合 Tasks 插件添加完成日期。

所以写了这个 TP 脚本,用来在切换完成状态的同时,添加完成日期
同时支持:

  1. 将纯文本转为任务项目
  2. 在完成/未完成状态之间切换
  3. 在空列表和空任务之间切换

只需要把下面这个 TP 脚本文档放进库中,在 Templater 插件里注册成快捷键(我直接覆盖了默认的 Ctrl+L 按键)即可。

TP 脚本:切换任务状态.md

直接下载:ToggleTask 切换任务状态.md

<%*
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;

// 获取当前行
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);

// 1. 将普通文本或普通列表转换成未完成的任务
if (!/^[\s]*- \[[ x-]\]/.test(line)) {
    // 将普通文本或普通列表转换为未完成的任务
    let newLine;
    if (line.startsWith('- ') || line.trim().startsWith('- ')) {
        // 如果是普通列表项,在破折号后插入任务标记
        newLine = line.replace(/^(\s*- )/, '$1[ ] ');
    } else {
        // 如果是普通文本,转换为任务,保留原有缩进
        const indentation = line.match(/^\s*/)[0];  // 获取开头的空白字符
        newLine = `${indentation}- [ ] ${line.trim()}`;
    }
    editor.replaceRange(newLine, { line: cursor.line, ch: 0 }, { line: cursor.line, ch: line.length });
    // 将光标移动到行末尾
    editor.setCursor({ line: cursor.line, ch: newLine.length });
    return;
}

// ! 检查当前任务状态
let newLine = line;
if (line.includes('[ ]')) {
    // 如果是未完成状态,先检查是否有内容
    if (line.replace('- [ ]', '').trim().length > 0) {
        // 标记为完成状态并添加日期
        const today = new Date();
        const dateStr = today.toISOString().split('T')[0].replace(/-/g, '-');
        newLine = newLine.replace(/\[ \]/, '[x]');
        newLine = newLine + ` ✅ ${dateStr}`; // 这里添加日期,去掉行末空格
    } 
    // 否则切换回普通列表
    else {
        // 如果没有内容,直接删除任务标记
        newLine = newLine.replace(/\[ \]/, '');
    }
} else if (line.includes('[x]')) {
    // 如果是完成状态,切换回未完成状态
    newLine = newLine.replace(/\[[x]\]/, '[ ]');
    newLine = newLine.replace(/\s*✅\s*\d{4}-\d{2}-\d{2}\s*$/, '');
} else {
    // 如果是其他状态,切换为未完成状态
    newLine = newLine.replace(/\[[ x]\]/, '[ ]');
}

// 替换当前行——注意是 trim 后的长度,不然可能导致多一个空格
editor.replaceRange(
    newLine.trimEnd(),
    { line: cursor.line, ch: 0 },
    { line: cursor.line, ch: line.trimEnd().length }
);

-%>

为什么不直接用原生的命令/Tasks 的命令

为了做到这样的效果:
a80b3e83-2dcc-4448-91dd-35e107419645

方便给任务创建子列表进行说明,在「列表/任务/完成任务」之间自由切换。

而原生的 Tasks Toggle Task 命令对空任务使用的时候,也会直接添加完成日期:
image
不能做到切换回列表。

假如我有任务a,a1,a11,每个任务一个页面。
我想通过每个笔记的页面属性「父任务」把它们连接起来。
比如a11的「父任务」是a1,a1的「父任务」是a。
如下图:
image

那有没有代码方法,把这三个任务放在一个多层级显示的列表中呢?
如下图:
image

可以的,提供个思路:
用 Dataview JS 写一个脚本,先根据 父任务 属性筛选出所有子任务页面,
然后提取子任务页面中的任务,作为第一层任务。

然后再基于子任务递归,提取出第二层任务。

但是这个需求比较特殊,我感觉没有现成的 tasklist 或者 tasks 功能可用,需要自己定制。
写起来可能有点复杂,详询AI。

如果需求不是这么强烈的话,建议简化一下,降低要求。
另外 Bon 佬最近在写的 Tasks Genius 插件也挺适合复杂任务管理,可以了解看看。

1 个赞

好,各位佬 牛 牛 牛。

togglelist 这个插件就实现了切换功能
可以自己设置多种切换. 从默认文字切换N个状态. 或者只在一两个状态中切换 都可以设置分组. 一组一种切换逻辑

{PARAGRAPH}
 
- 
- [ ] 
- [x] || ✅ {time:: YYYY-MM-DD}
- [-] || ❌ {time:: YYYY-MM-DD}
- [-] || ➖️ {time:: YYYY-MM-DD}
- [?] || ❓️ {time:: YYYY-MM-DD}

空白就是普通文本的意思.
普通文本 无序列表 任务 完成 失败 取消 有问题 这7种状态切换.

我个人没有这种复杂需求,不过感谢推荐 :+1:t2:

我让AI进行了修改.实现的切换状态是

<%*
// --- Setup ---
const editor = app.workspace.activeEditor.editor;
const cursor = editor.getCursor();
const currentLineNumber = cursor.line;
const line = editor.getLine(currentLineNumber);

// --- Helper: Get Timestamp ---
function getTimestamp() {
    const now = new Date();
    const year = now.getFullYear();
    const month = (now.getMonth() + 1).toString().padStart(2, '0');
    const day = now.getDate().toString().padStart(2, '0');
    const hours = now.getHours().toString().padStart(2, '0');
    const minutes = now.getMinutes().toString().padStart(2, '0');
    const seconds = now.getSeconds().toString().padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

// --- Helper: Get Indent ---
function getIndent(lineContent) {
    const match = lineContent.match(/^(\s*)/);
    return match ? match[1] : "";
}

// --- Helper: Replace Line and Set Cursor ---
function replaceAndExit(newLine) {
    editor.replaceRange(newLine, { line: currentLineNumber, ch: 0 }, { line: currentLineNumber, ch: line.length });
    editor.setCursor({ line: currentLineNumber, ch: newLine.length });
    // Stop script execution after performing an action
    return;
}

// --- Regex Definitions ---
const indent = getIndent(line);
let coreText = "";
let newLine = "";
let match;

// STANDARD FORMATS (Strict: require full timestamp and match end of line $)
const regexDoneStd = /^\s*- \[x\] (.*?) ✅ (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/;
const regexFailedStd = /^\s*- \[-\] (.*?) ❌ (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/;
const regexCancelledStd = /^\s*- \[-\] (.*?) ➖️ (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/;
const regexQuestionStd = /^\s*- \[\?\] (.*?) ❓️ (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/;
const regexTodo = /^\s*- \[ \](.*)/; // Capture text after marker
const regexUnorderedList = /^\s*- (?!\[[ x?-]\])(.*)/; // Capture text after marker

// VARIANT FORMATS (for Normalization)
const regexDoneDateOnly = /^\s*- \[x\] (.*?) ✅ (\d{4}-\d{2}-\d{2})$/;
const regexFailedDateOnly = /^\s*- \[-\] (.*?) ❌ (\d{4}-\d{2}-\d{2})$/;
const regexCancelledDateOnly = /^\s*- \[-\] (.*?) ➖️ (\d{4}-\d{2}-\d{2})$/;
const regexQuestionDateOnly = /^\s*- \[\?\] (.*?) ❓️ (\d{4}-\d{2}-\d{2})$/;
// Matches "- [x] Text" but only if NOT followed by a standard emoji/timestamp pattern
const regexDonePlain = /^\s*- \[x\](?! .*?✅ \d{4}-\d{2}-\d{2})(?! .*?❌ \d{4}-\d{2}-\d{2})(?! .*?➖️ \d{4}-\d{2}-\d{2})(?! .*?❓️ \d{4}-\d{2}-\d{2})(.*)/;


// --- 1. NORMALIZATION STEP: Convert variants to Standard Format ---

// Normalize DateOnly formats
if ((match = line.match(regexDoneDateOnly))) {
    coreText = match[1].trim();
    newLine = `${indent}- [x] ${coreText} ✅ ${getTimestamp()}`; // Standardize Done
    return replaceAndExit(newLine);
} else if ((match = line.match(regexFailedDateOnly))) {
    coreText = match[1].trim();
    newLine = `${indent}- [-] ${coreText} ❌ ${getTimestamp()}`; // Standardize Failed
    return replaceAndExit(newLine);
} else if ((match = line.match(regexCancelledDateOnly))) {
    coreText = match[1].trim();
    newLine = `${indent}- [-] ${coreText} ➖️ ${getTimestamp()}`; // Standardize Cancelled
    return replaceAndExit(newLine);
} else if ((match = line.match(regexQuestionDateOnly))) {
    coreText = match[1].trim();
    newLine = `${indent}- [?] ${coreText} ❓️ ${getTimestamp()}`; // Standardize Question
    return replaceAndExit(newLine);
}
// Normalize Plain Done Task (e.g., from simple checkbox click)
else if ((match = line.match(regexDonePlain))) {
    coreText = match[1].trim();
    newLine = `${indent}- [x] ${coreText} ✅ ${getTimestamp()}`; // Standardize Done
    return replaceAndExit(newLine);
}

// --- 2. MAIN CYCLE STEP: Switch between Standard Formats ---

// Add state symbols and time stamps based on current state
if ((match = line.match(regexDoneStd))) {
    // Current: Done -> Next: Failed
    coreText = match[1].trim(); // Use captured group
    newLine = `${indent}- [-] ${coreText} ❌ ${getTimestamp()}`;
    return replaceAndExit(newLine);
} else if ((match = line.match(regexFailedStd))) {
    // Current: Failed -> Next: Cancelled
    coreText = match[1].trim();
    newLine = `${indent}- [-] ${coreText} ➖️ ${getTimestamp()}`;
    return replaceAndExit(newLine);
} else if ((match = line.match(regexCancelledStd))) {
    // Current: Cancelled -> Next: Question
    coreText = match[1].trim();
    newLine = `${indent}- [?] ${coreText} ❓️ ${getTimestamp()}`;
    return replaceAndExit(newLine);
} else if ((match = line.match(regexQuestionStd))) {
    // Current: Question -> Next: Plain Text
    coreText = match[1].trim();
    newLine = `${indent}${coreText}`; // No emoji or timestamp
    // Handle case where coreText might be empty
    if (newLine === indent) {
        newLine = indent.trimEnd(); // Avoid line with only whitespace
    }
    return replaceAndExit(newLine);
} else if ((match = line.match(regexTodo))) {
    // Current: Todo -> Next: Done
    coreText = match[1].trim();
    newLine = `${indent}- [x] ${coreText} ✅ ${getTimestamp()}`;
    return replaceAndExit(newLine);
} else if ((match = line.match(regexUnorderedList))) {
    // Current: Unordered List -> Next: Todo
    coreText = match[1].trim();
    newLine = `${indent}- [ ] ${coreText}`;
    return replaceAndExit(newLine);
} else {
    // Current: Plain Text -> Next: Unordered List
    coreText = line.trim(); // Get original text
    if (coreText === "") {
        // If line was empty or just whitespace, create a simple list item
        newLine = `${indent}- `;
    } else {
        // Otherwise, convert plain text to list item
        newLine = `${indent}- ${coreText}`;
    }
    return replaceAndExit(newLine);
}

-%>

1 个赞

@不知名 的启发下,我也调整了一下,增加了可选项 isCycle
isCycling == true 时,可在「空任务/完成任务/取消任务」三种状态之间来回切换。

<%*
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;

// 是否循环切换任务状态
const isCycling = true;

const cycleMapping = {
    '[ ]': '[x]',
    '[x]': '[-]',
    '[-]': '[ ]'
}

// 获取当前行
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);

let newLine = line;

// 1. 将普通文本或普通列表转换成未完成的任务
if (!/^[\s]*- \[[ x-]\]/.test(line)) {
    // 将普通文本或普通列表转换为未完成的任务
    if (line.endsWith('- ')) {
        newLine = line.replace('- ', '- [ ] ');
    } else if (line.startsWith('- ') || line.trim().startsWith('- ')) {
        // 如果是普通列表项,在破折号后插入任务标记
        newLine = line.replace(/^(\s*- )/, '$1[ ] ');
    } else {
        // 如果是普通文本,转换为任务,保留原有缩进
        const indentation = line.match(/^\s*/)[0];  // 获取开头的空白字符
        newLine = `${indentation}- [ ] ${line.trim()}`;
    }
    editor.replaceRange(newLine, { line: cursor.line, ch: 0 }, { line: cursor.line, ch: line.length });
    // 将光标移动到行末尾
    editor.setCursor({ line: cursor.line, ch: newLine.length });
    return;
}

// ! 检查当前任务状态

// 空任务的情况——切换回列表
if (line.endsWith('- [ ] ')) {
    newLine = line.replace('- [ ] ', '- ');
    editor.replaceRange(
        newLine.trimEnd(),
        { line: cursor.line, ch: 0 },
        { line: cursor.line, ch: line.trimEnd().length }
    );
    return;
}

const today = moment().format('YYYY-MM-DD');

if (!isCycling) {
    if (line.includes('[ ]')) {
        // 标记为完成状态并添加日期
        newLine = newLine.replace(/\[ \]/, '[x]');
        newLine = newLine + ` ✅ ${today}`; // 这里添加日期,去掉行末空格
    } else if (line.includes('[x]')) {
        // 如果是完成状态,切换回未完成状态
        newLine = newLine.replace(/\[[x]\]/, '[ ]');
        newLine = newLine.replace(/\s*✅\s*\d{4}-\d{2}-\d{2}\s*$/, '');
    } else {
        // 如果是其他状态,切换为未完成状态
        newLine = newLine.replace(/\[[ x]\]/, '[ ]');
    }
} else {
    const currentStatus = line.match(/- \[.\]/)[0]?.replace('- ', '');

    console.log(`currentStatus: ${currentStatus}`);

    if (!currentStatus) {
        newLine = '- [ ]' + newLine;
    }

    const nextStatus = cycleMapping[currentStatus];

    // 移除已有的 ✅xxxx-xx-xx 或 ❌xxxx-xx-xx
    newLine = line.replace(/ (✅|❌) \d{4}-\d{2}-\d{2}/g, '');

    if (nextStatus === '[x]') {
        // 添加完成标记和日期
        newLine = newLine.replace(/- \[.\]/, `- [x]`) + ` ✅ ${today}`;
    } else if (nextStatus === '[-]') {
        // 添加否决标记和日期
        newLine = newLine.replace(/- \[.\]/, `- [-]`) + ` ❌ ${today}`;
    } else {
        // 仅切换状态,不加日期
        newLine = newLine.replace(/- \[.\]/, `- ${nextStatus}`);
    }
}

// console.log(`>>${newLine}<<`);

// 替换当前行——注意是 trim 后的长度,不然可能导致多一个空格
editor.replaceRange(
    newLine.trimEnd(),
    { line: cursor.line, ch: 0 },
    { line: cursor.line, ch: line.trimEnd().length }
);

// 然后运行一下重新排序的任务
// window.open("obsidian://advanced-uri?vault=Obsinote&commandid=templater-obsidian%253A_global%252Ftemplates%252Fcommand%252FtpRunner.md&template=ReOrder List 重排序列表");
// 这里是不是该用那个用户函数了捏…… 🤔

-%>