【Dataview 入门介绍】DV 脚本是什么?怎么用?

和之前的 这个CSS入门教程 一样,本贴基本上是作为一个「给完全不懂的人看的基础入门教程」,以便在今后的其他 DV 分享中直接引用,而不必重复说明。

如果你已经会用 DV 了,则不需要看这个非常粗浅的入门说明。

但若你看到一些帖子只放了所谓”DV 代码“,茫然无措,那么这篇文章是为你准备的。

DV 是什么?

DV 是 Dataview 的简称,这是一款专门用来做 Obsidian 内「数据查询」的插件。

仓库:blacksmithgu/obsidian-dataview: A data index and query language over Markdown files, for https://obsidian.md/.
文档:Dataview

插件说明:
将您的 Obsidian Vault 视为您可以从中查询的数据库。
提供 JavaScript API 和基于管道的查询语言,用于从 Markdown 页面筛选、排序和提取数据

例如,它可以用表格呈现某个文件夹内的笔记和元数据:

展现最近创建和修改的笔记:

详见: 【DV脚本】三合一的最近笔记展示脚本 - 经验分享 - Obsidian 中文论坛

也可以在单个笔记内快速收集带有关键字的段落:

详见: PKMer_通过 Dataview 实现汇总显示笔记内的关键信息

——以上这些功能,都需要用 dataview代码块 实现。


有两种类型代码块,一种是 dataview 查询块:

```dataview  
table time-played, length, rating
from "games"
sort rating desc
```

另一种是 dataviewjs 脚本:

```dataviewjs
dv.paragraph("Hello, world")
```

前者算是一种自定义语法的查询语言,后者则是使用 Javascript 脚本语言编程,并调用 dv 提供的函数来呈现结果。

想要知道区别,就看三个 ` 后面跟着的是 dataview 还是 dataviewjs 就行了。

想进一步了解可以看官方的文档:
DQL, JS and Inlines - Dataview


怎么用?

如果你看到一个帖子附上了 dataview 代码,想要在自己的笔记中实现类似的效果,你需要:

  1. 在 Obsidian 内安装 Dataview 插件

  2. 如果是 dataviewjs 代码,需要在插件设置里勾选脚本功能:

  3. 将代码块粘贴进自己的笔记


另外,由于 dataview 查询常常和路径、文件夹名称挂钩,所以如果没有显示出预期的结果,需要检查里面的路径,替换成自己的实际路径。

在 dv 里,通常是 FROM 后面跟着的内容。
例如上面举例的:

```dataview  
table time-played, length, rating
from "games"
sort rating desc
```

这里是 from "games" 即从 games/ 这个文件夹中查找笔记。
而如果我本地是放在另一个文件夹里,就需要改成相应的 from "游戏数据库/我的游戏" 这样的路径。

Dataview | obsidian文档咖啡豆版 中有关于 Dataview 更详细的用例,以及语法解释。
想要真正了解它的话,尤其推荐这篇:Dataview进阶(1)基础字段 | obsidian文档咖啡豆版


进阶用法:单独的 JS 文件

有时候,我们会写一个单独的 js 文件,并且用它来作为 dataviewjs 查询。
这可能出于多种原因,大多数情况下是为了==能方便地在多个笔记中复用同一套代码==,并且可以很容易地更新查询,不需要改动原笔记。

这种情况下,需要先在仓库里创建一个 脚本.js 文件,然后将 JS 代码粘贴进去,保存。

然后,笔记内插入这样的 dvjs 代码块:

```dataviewjs
dv.view("脚本")
```

它会读取你笔记库内 脚本.js 的内容,并且渲染出对应脚本执行后的结果。
注意,尽量不要重名。

你也可以写成完整的路径,例如:脚本/Dataview脚本/一个脚本.js 的话,就写成:

```dataviewjs
dv.view("脚本/Dataview脚本/一个脚本.js")
```

更高级的脚本文件夹

最后,还有更进阶的一种方式,如果你还想自己定义 CSS 样式,可以把脚本和 css 放进一个文件夹,例如这样两个文件:
脚本/Dataview脚本/漂亮的查询表格/view.js
脚本/Dataview脚本/漂亮的查询表格/view.css

这样一来,你可以直接通过文件夹的名称来调用:

```dataviewjs
dv.view("漂亮的查询表格")
```

更进一步,还可以给脚本传递参数,来实现不同的功能开关。
但是这显然超出入门范畴了,这里就略过不表。

感兴趣也可以看官方的说明:Codeblock Reference - Dataview

:warning: 必要的警告:
由于脚本功能本身有风险,所以最好不要随便什么脚本都往库里放。
因为 Javascript 作为脚本语言,可以做到包括但不限于修改笔记内容等各种事项。

如果不放心,可以先把脚本粘贴给 AI,询问它这段脚本的作用以及是否有风险。

5 个赞

大佬,以下dataviewjs代码段是我通过deepseek生成的,它实现了显示文件夹“01_P”下的笔记的时间轴、通过任务计算进度以及剩余时间。我想请问,能否使该代码段实现按剩余时间排序的功能(超期的排在最前,剩余天数越少的排越前)?该代码段如下:

```dataviewjs
dv.paragraph("# 项目追踪");
const projects = dv.pages('"01_P"')
    .where(p => 
        dv.date(p.开始日期)?.isValid && 
        dv.date(p.截止日期)?.isValid
    );

dv.list(projects.map(p => {
    // 智能任务统计系统
    const totalTasks = p.file.tasks.length;
    const completedTasks = p.file.tasks.filter(t => t.completed).length;
    const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;

    // 动态时间轴生成
    const startDate = dv.date(p.开始日期);
    const endDate = dv.date(p.截止日期);
    const timeline = `🕒 ${startDate.toFormat("MM/dd")} → ${endDate.toFormat("MM/dd")}`;
    
    // 三维进度可视化系统
    const progressBlocks = '▮'.repeat(Math.floor(progress/10));
    const remainingBlocks = '▯'.repeat(10 - Math.floor(progress/10));
    const progressBar = `${progressBlocks}${remainingBlocks}`;
    
    // 智能倒计时模块
    const today = dv.date("now");
    const daysRemaining = endDate.diff(today, "days").days;
    const statusIcon = daysRemaining < 0 ? "⌛" : "⏳";
    const countdown = daysRemaining >= 0 
        ? `${statusIcon} 剩余${Math.floor(daysRemaining)}天` 
        : `${statusIcon} 超期${Math.abs(Math.floor(daysRemaining))}天`;

    return `${p.file.link} 
        | 时间轴:${timeline}
        | 进度:${progressBar} ${progress}%
        | ${countdown}`;
}));

实际效果如下:

项目追踪
06_GPIO 中断
| 时间轴:🕒 03/17 → 03/24
| 进度:▮▮▮▮▮▯▯▯▯▯ 50%
| ⏳ 剩余5天

可以的,把你的需求讲给 Deepseek 让它继续改就行。


另外,如果是项目管理/任务追踪,也可以参考这样的表格形式:

代码如下 :arrow_down:

直接拿过去肯定没法用,让 AI 帮你改成适合你的笔记形式的代码——对应的笔记属性格式之类的也可以询问大D老师,信息量都在代码里了,我不包答疑哈

const onlyProjectFolder = true;

const targetFolder = onlyProjectFolder?"1-Project":"";

let projects = dv.pages( `"${targetFolder}"` ).filter(p => 
{
    return p.PARA == "Project" && p.status != "Cancelled" && p.status != "No Status"
});

// 定义状态显示顺序和对应的 Emoji
const statusConfig = {
  "Focus": { emoji: "🔍" },
  "Drafting": { emoji: "✏️" },
  "In Progress": { emoji: "🚀" },
  "To Do": { emoji: "📝" },
  "Idea": { emoji: "💡" },
  "Pending": { emoji: "⏳" },
  "On Hold": { emoji: "⏸️" },
  "Completed": { emoji: "✅" }
};

// 获取状态列表(按定义顺序)
const statusOrder = Object.keys(statusConfig);

// 获取状态对应的显示文本(带 Emoji)
function getStatusDisplay(status) {
  if (statusConfig[status]) {
    return `${statusConfig[status].emoji} ${status}`;
  }
  return status; // 如果没有配置,保持原样
}

// 按状态分组
let groupedProjects = {};
projects.forEach(project => {
  if (!groupedProjects[project.status]) {
    groupedProjects[project.status] = [];
  }
  groupedProjects[project.status].push(project);
});

// 创建表格数据
let tableData = [];

// 获取当前日期
const today = new Date();
today.setHours(0, 0, 0, 0);

// 处理日期和倒计时的函数
function processDeadline(deadline) {
  // 检查是否为时间戳格式(数字)
  if (deadline && !isNaN(deadline)) {
    // 如果是数字(时间戳),转换为日期对象
    let deadlineDate = new Date(Number(deadline));
    
    // 格式化为 YYYY.MM.DD 格式
    let year = deadlineDate.getFullYear();
    let month = String(deadlineDate.getMonth() + 1).padStart(2, '0');
    let day = String(deadlineDate.getDate()).padStart(2, '0');
    let formattedDate = `${year}.${month}.${day}`;
    
    // 计算剩余天数
    const timeDiff = deadlineDate.getTime() - today.getTime();
    const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
    
    return formattedDate + getCountdownText(daysDiff);
  }
  
  // 保留原始日期显示
  let deadlineDisplay = deadline || "";
  
  if (deadline) {
    // 解析截止日期
    try {
      // 尝试解析日期
      let deadlineDate = new Date(deadline);
      
      // 检查日期是否有效
      if (!isNaN(deadlineDate.getTime())) {
        // 计算剩余天数
        deadlineDate.setHours(0, 0, 0, 0);
        const timeDiff = deadlineDate.getTime() - today.getTime();
        const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
        
        return deadlineDisplay + getCountdownText(daysDiff);
      }
    } catch (e) {
      // 解析失败,不添加倒计时信息
    }
  }
  
  return deadlineDisplay;
}

// 获取倒计时文本
function getCountdownText(daysDiff) {
  if (daysDiff > 0) {
    return `<br>还有 ${daysDiff} 天`;
  } else if (daysDiff === 0) {
    return `<br>就是今天`;
  } else {
    return `<br>💀 已过期!`;
  }
}

// 按照预定义顺序添加状态组
for (let status of statusOrder) {
  // 如果没有该状态的项目,跳过
  if (!groupedProjects[status] || groupedProjects[status].length === 0) {
    continue;
  }
  
  // 添加状态行
  tableData.push([
    `<span class="proj-status">${getStatusDisplay(status)}</span>`,
    "", "", "", ""
  ]);
  
  // 添加该状态下的项目
  groupedProjects[status].forEach(project => {
    tableData.push([
      project.file.link,
      project.passion=="Intense"?"🔥On Fire!":project.passion,
      project.type,
      `<progress value="${project.progress}" max="${project.target ? project.target : 100}"></progress><br>进度:${Math.round((project.progress / (project.target?project.target:100)) * 100)}%`,
      processDeadline(project.deadline)
    ]);
  });
}

// 处理未在预定义顺序中的状态
let otherStatuses = Object.keys(groupedProjects).filter(status => !statusOrder.includes(status));
for (let status of otherStatuses) {
  // 添加状态行
  tableData.push([
    `<span class="proj-status">${getStatusDisplay(status)}</span>`,
    "", "", "", ""
  ]);
  
  // 添加该状态下的项目
  groupedProjects[status].forEach(project => {
    tableData.push([
      project.file.link,
      project.passion,
      project.type,
      `<progress value="${project.progress}" max="${project.target ? project.target : 100}"></progress><br>进度:${Math.round((project.progress / (project.target?project.target:100)) * 100)}%`,
      processDeadline(project.deadline)
    ]);
  });
}

// 创建单个表格
dv.table(["项目", "兴致", "类型", "进度", "死线"], tableData);

// 添加样式类
dv.container.classList.add("cards-align-top");

// 添加交错行样式 - 使用 Dataview 的方式
const styleElement = dv.container.createEl('style', {
  text: `
    .dataview.table-view-table tr:nth-child(even):not(:first-child) {
      background-color: rgba(0, 0, 0, 0.05);
    }
  `
});
1 个赞