Moy
1
演示

说明
使用默认的「切换待办事项」命令时,无法配合 Tasks 插件添加完成日期。
所以写了这个 TP 脚本,用来在切换完成状态的同时,添加完成日期。
同时支持:
- 将纯文本转为任务项目
- 在完成/未完成状态之间切换
- 在空列表和空任务之间切换
只需要把下面这个 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 的命令
为了做到这样的效果:

方便给任务创建子列表进行说明,在「列表/任务/完成任务」之间自由切换。
而原生的 Tasks Toggle Task 命令对空任务使用的时候,也会直接添加完成日期:

不能做到切换回列表。
美人儿呀
(Wrwe)
2
假如我有任务a,a1,a11,每个任务一个页面。
我想通过每个笔记的页面属性「父任务」把它们连接起来。
比如a11的「父任务」是a1,a1的「父任务」是a。
如下图:

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

Moy
3
可以的,提供个思路:
用 Dataview JS 写一个脚本,先根据 父任务
属性筛选出所有子任务页面,
然后提取子任务页面中的任务,作为第一层任务。
然后再基于子任务递归,提取出第二层任务。
但是这个需求比较特殊,我感觉没有现成的 tasklist 或者 tasks 功能可用,需要自己定制。
写起来可能有点复杂,详询AI。
如果需求不是这么强烈的话,建议简化一下,降低要求。
另外 Bon 佬最近在写的 Tasks Genius 插件也挺适合复杂任务管理,可以了解看看。
1 个赞
不知名
5
togglelist 这个插件就实现了切换功能
可以自己设置多种切换. 从默认文字切换N个状态. 或者只在一两个状态中切换 都可以设置分组. 一组一种切换逻辑
不知名
6
{PARAGRAPH}
-
- [ ]
- [x] || ✅ {time:: YYYY-MM-DD}
- [-] || ❌ {time:: YYYY-MM-DD}
- [-] || ➖️ {time:: YYYY-MM-DD}
- [?] || ❓️ {time:: YYYY-MM-DD}
空白就是普通文本的意思.
普通文本 无序列表 任务 完成 失败 取消 有问题 这7种状态切换.
不知名
8
我让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 个赞
Moy
9
在 @不知名 的启发下,我也调整了一下,增加了可选项 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 重排序列表");
// 这里是不是该用那个用户函数了捏…… 🤔
-%>