Dataviewjs:卡片样式的倒数日表示

前提

  • #倒数日 的标签对某些任务进行标注,表示其倒数日的性质
  • 这里使用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);
}

演示

这里是本库的演示,主要实现功能为:

  • 以卡片形式显示倒数日
  • 右键卡片,确定是否删除,点击确认会删除 其索引的倒数日任务的对应行数

PixPin_2025-06-17_19-08-43

其他

如果有其他需求,把代码扔进ds-r1 或者Gemini或者使用Vscode 的Copilot(GPT 4.1)去根据自己的需求更改,人起到一个响应和调试 的作用就行。

如果有能力,Gemini比ds-r1我认为是要准确不少

1 个赞


好用,