前提
- 用
#倒数日
的标签对某些任务进行标注,表示其倒数日的性质 - 这里使用Tasks插件的start date和due date 表示该事件的2个时间节点,当前代码只考虑了2个情况,即一个时间点(due date)和时间段的情况(start date → due date)
按照上述的设置,下面是使用卡片显示倒数日的dataviewjs
代码,由Gemini生成
代码
// --- 卡片式倒数日 ---
// 1. 收集所有带 #倒数日 标签的任务,包括已完成和未完成的。
// 2. 解析任务中的开始日期(🛫)和截止日期(📅)。
// 3. 计算与今天的日期差距,生成状态描述。
// 4. 按日期对任务进行排序(已过 -> 今天 -> 未来)。
// 5. 以卡片形式动态渲染每个任务。
// 6. 为每个卡片添加右键点击删除功能,并包含确认提示。
// --- 数据收集与处理 ---
const pages = dv.pages().file;
const tasks = [];
// 1. 收集所有带有 #倒数日 标签的任务 (不区分是否完成)
for (let page of pages) {
const fileTasks = page.tasks;
if (!fileTasks) continue;
for (let task of fileTasks) {
if (task.text.includes("#倒数日")) {
tasks.push(task);
}
}
}
// 获取今天的日期(去掉时间部分,用于精确比较)
const today = moment().startOf('day');
// 2. 处理任务文本,提取开始和截止日期
const processed = tasks.map(task => {
let text = task.text;
// 提取截止日期 (Due Date)
let dueDate = null;
const dueDateMatch = text.match(/📅\s*(\d{4}-\d{2}-\d{2})/);
if (dueDateMatch) {
dueDate = dueDateMatch[1];
text = text.replace(dueDateMatch[0], "").trim();
}
// 提取开始日期 (Start Date)
let startDate = null;
const startDateMatch = text.match(/🛫\s*(\d{4}-\d{2}-\d{2})/);
if (startDateMatch) {
startDate = startDateMatch[1];
text = text.replace(startDateMatch[0], "").trim();
}
// 清理标签
text = text.replace(/#倒数日/g, "").trim();
// 3. 计算日期差距并生成状态
const targetDate = dueDate ? moment(dueDate, "YYYY-MM-DD") : null;
let status = "日期无效";
let diff = 0;
if (targetDate && targetDate.isValid()) {
diff = targetDate.diff(today, 'days');
if (diff > 0) {
status = `还有 ${diff} 天`;
} else if (diff < 0) {
status = `已过 ${-diff} 天`;
} else {
status = "就是今天!";
}
}
return {
text: text,
rawDueDate: dueDate,
rawStartDate: startDate,
status: status,
diff: diff, // 用于排序
originalTask: task
};
});
// 过滤掉没有有效截止日期的任务
const validTasks = processed.filter(t => t.rawDueDate);
// 4. 按日期差异进行排序 (已过 -> 今天 -> 未来)
validTasks.sort((a, b) => a.diff - b.diff);
// --- 渲染卡片 ---
// 创建一个容器来包裹所有卡片,方便使用 Flexbox 布局
const container = dv.container;
container.style.display = "flex";
container.style.flexWrap = "wrap";
container.style.gap = "15px"; // 卡片之间的间距
// 5. 遍历排序后的任务,为每个任务创建卡片
for (const task of validTasks) {
// 创建卡片元素
const card = dv.el("div", "");
card.style.border = "1px solid var(--background-modifier-border)";
card.style.borderRadius = "8px";
card.style.padding = "16px";
card.style.backgroundColor = "var(--background-secondary)";
card.style.display = "flex";
card.style.flexDirection = "column";
card.style.justifyContent = "space-between";
card.style.width = "220px";
card.style.minHeight = "150px";
card.style.cursor = "pointer";
card.style.transition = "transform 0.2s ease, box-shadow 0.2s ease";
// 鼠标悬停效果
card.onmouseenter = () => {
card.style.transform = "translateY(-3px)";
card.style.boxShadow = "0 4px 12px rgba(0,0,0,0.1)";
};
card.onmouseleave = () => {
card.style.transform = "translateY(0)";
card.style.boxShadow = "none";
};
// 6. 添加右键删除功能
card.oncontextmenu = async (e) => {
e.preventDefault(); // 阻止默认的右键菜单
if (!confirm(`确定要删除任务 "${task.text}" 吗?`)) return;
try {
const file = app.vault.getAbstractFileByPath(task.originalTask.path);
if (!file) throw new Error("File not found");
const content = await app.vault.read(file);
const lines = content.split('\n');
// 安全删除,防止行号越界
if (lines.length > task.originalTask.line) {
lines.splice(task.originalTask.line, 1);
await app.vault.modify(file, lines.join('\n'));
new Notice(`任务 "${task.text}" 已删除。`);
// 可选:刷新视图,但通常 dataview 会自动处理
} else {
new Notice("删除失败:任务行号无效。");
}
} catch (error) {
new Notice("删除任务时出错。");
console.error(error);
}
};
// --- 卡片内部布局 ---
// 状态
const statusEl = dv.el("div", task.status);
statusEl.style.fontSize = "1.2em";
statusEl.style.fontWeight = "bold";
statusEl.style.color = "var(--text-accent)";
statusEl.style.marginBottom = "10px";
statusEl.style.textAlign = "center";
// 内容
const contentEl = dv.el("div", "");
// 如果任务已完成,则添加删除线
contentEl.innerHTML = task.originalTask.completed ? `<s>${task.text}</s>` : task.text;
contentEl.style.fontSize = "1em";
contentEl.style.flexGrow = "1"; // 让内容区域占据多余空间
contentEl.style.marginBottom = "15px";
contentEl.style.textAlign = "center";
contentEl.style.wordWrap = "break-word";
// 日期
const dateEl = dv.el("div", "");
let dateText = "";
if (task.rawStartDate) {
dateText += `🛫 ${task.rawStartDate}`;
}
// 如果有开始日期,则添加分隔符
if (task.rawStartDate && task.rawDueDate) {
dateText += " → ";
}
if (task.rawDueDate) {
dateText += `📅 ${task.rawDueDate}`;
}
dateEl.textContent = dateText;
dateEl.style.fontSize = "0.85em";
dateEl.style.color = "var(--text-muted)";
dateEl.style.textAlign = "center";
// 将各部分添加到卡片中
card.appendChild(statusEl);
card.appendChild(contentEl);
card.appendChild(dateEl);
// 将卡片添加到主容器中
container.appendChild(card);
}
演示
这里是本库的演示,主要实现功能为:
- 以卡片形式显示倒数日
- 右键卡片,确定是否删除,点击确认会删除 其索引的倒数日任务的对应行数
其他
如果有其他需求,把代码扔进ds-r1 或者Gemini或者使用Vscode 的Copilot(GPT 4.1)去根据自己的需求更改,人起到一个响应和调试 的作用就行。
如果有能力,Gemini比ds-r1我认为是要准确不少