Skip to content

DataviewJS 高级用法

DataviewJS 是 Dataview 插件的高级模式,允许你使用完整的 JavaScript 编程能力来查询和展示笔记数据。相比 DQL 查询语言,DataviewJS 提供了更强的灵活性和表现力。

何时使用 DataviewJS

场景DQLDataviewJS说明
简单列表/表格查询DQL 更简洁直观
条件渲染根据数据动态决定显示内容
跨笔记计算对多笔记数据进行数学运算
交互式 UI按钮、表单、动态筛选
自定义 HTML完全控制渲染输出
复杂分组与嵌套有限多级分组、嵌套表格

核心对象

DataviewJS 环境提供了以下内置对象:

javascript
// dv 对象 — Dataview 核心 API
dv.pages()          // 获取所有页面
dv.page(path)       // 获取单个页面
dv.current()        // 获取当前页面
dv.date(text)        // 解析日期
dv.duration(text)    // 解析时长
dv.fileLink(path)   // 创建文件链接

// 便捷方法
dv.header(level, text)     // 渲染标题
dv.paragraph(text)         // 渲染段落
dv.list(items)             // 渲染列表
dv.table(headers, rows)    // 渲染表格
dv.taskList(tasks)         // 渲染任务列表
dv.markdownTable(headers, rows) // 生成 Markdown 表格文本
dv.span(text)              // 渲染内联元素
dv.el(tag, text)           // 渲染任意 HTML 元素

基础示例

动态笔记仪表板

dataviewjs
const pages = dv.pages('"daily"')
  .where(p => p.mood)
  .sort(p => p.file.day, 'desc')
  .limit(7);

if (pages.length === 0) {
  dv.paragraph("暂无心情记录数据");
} else {
  dv.header(3, "📊 最近 7 天心情追踪");
  
  dv.table(
    ["日期", "心情", "天气", "关键词"],
    pages.map(p => [
      p.file.link,
      p.mood,
      p.weather || "—",
      p.keywords ? p.keywords.join(", ") : "—"
    ])
  );
  
  // 统计心情分布
  const moodCounts = {};
  for (let p of pages) {
    moodCounts[p.mood] = (moodCounts[p.mood] || 0) + 1;
  }
  
  dv.header(4, "心情分布");
  const moodBar = Object.entries(moodCounts)
    .map(([mood, count]) => `${mood} ${"█".repeat(count)} ${count}`)
    .join("\n");
  dv.paragraph(moodBar);
}

项目进度仪表板

dataviewjs
const projects = dv.pages('"projects"')
  .where(p => p.status);

dv.header(3, "🎯 项目进度总览");

// 分组显示
const groups = {
  "🟢 进行中": projects.where(p => p.status === "active"),
  "🟡 暂停": projects.where(p => p.status === "paused"),
  "🔴 逾期": projects.where(p => p.deadline && p.deadline < dv.date("today") && p.status !== "done"),
  "✅ 已完成": projects.where(p => p.status === "done")
};

for (let [label, group] of Object.entries(groups)) {
  if (group.length === 0) continue;
  dv.header(4, `${label} (${group.length})`);
  
  dv.table(
    ["项目", "进度", "截止日期", "优先级"],
    group.map(p => {
      const daysLeft = p.deadline 
        ? Math.floor((p.deadline - dv.date("today")).days) 
        : null;
      const urgency = daysLeft !== null 
        ? (daysLeft < 0 ? "🔴 逾期" : daysLeft < 7 ? "🟡 紧急" : "🟢 正常")
        : "—";
      return [
        p.file.link,
        p.progress ? `${p.progress}%` : "—",
        p.deadline ? `${p.deadline} (${urgency})` : "—",
        p.priority || "—"
      ];
    })
  );
}

高级技巧

1. 交互式筛选

dataviewjs
const allPages = dv.pages('"notes"')
  .where(p => p.tags)
  .sort(p => p.file.name);

// 获取所有标签
const allTags = [...new Set(allPages.flatMap(p => p.tags))].sort();

// 创建筛选 UI
const container = dv.container || dv.el('div');
const filterDiv = container.createEl('div', { 
  cls: 'dvjs-filter-bar',
  attr: { style: 'margin-bottom: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;' }
});

let selectedTag = null;

// 渲染标签按钮
for (let tag of allTags) {
  const btn = filterDiv.createEl('button', {
    text: tag,
    cls: 'dvjs-tag-btn',
    attr: {
      style: 'padding: 4px 12px; border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; background: var(--background-secondary);'
    }
  });
  btn.onclick = () => {
    selectedTag = selectedTag === tag ? null : tag;
    renderList();
    // 更新按钮样式
    filterDiv.querySelectorAll('button').forEach(b => {
      b.style.background = b.textContent === selectedTag 
        ? 'var(--interactive-accent)' 
        : 'var(--background-secondary)';
      b.style.color = b.textContent === selectedTag 
        ? 'var(--text-on-accent)' 
        : 'var(--text-normal)';
    });
  };
}

const listDiv = container.createEl('div');

function renderList() {
  listDiv.empty();
  const filtered = selectedTag 
    ? allPages.where(p => p.tags && p.tags.includes(selectedTag))
    : allPages;
  
  if (filtered.length === 0) {
    listDiv.createEl('p', { text: '没有匹配的笔记' });
    return;
  }
  
  const ul = listDiv.createEl('ul');
  for (let p of filtered) {
    const li = ul.createEl('li');
    const a = li.createEl('a', { 
      text: p.file.name,
      attr: { 
        href: p.file.path,
        class: 'internal-link'
      }
    });
    if (p.tags && p.tags.length > 0) {
      li.createEl('span', { 
        text: ` · ${p.tags.join(', ')}`,
        attr: { style: 'color: var(--text-muted); font-size: 0.85em;' }
      });
    }
  }
}

renderList();

2. 数据可视化(进度条)

dataviewjs
const tasks = dv.pages('"tasks"')
  .where(p => p.file.tasks)
  .file.tasks
  .where(t => !t.completed);

const byPriority = {
  "🔴 高": tasks.where(t => t.priority === "high"),
  "🟡 中": tasks.where(t => t.priority === "medium"),
  "🟢 低": tasks.where(t => t.priority === "low" || !t.priority)
};

dv.header(3, "📋 任务优先级分布");

for (let [label, group] of Object.entries(byPriority)) {
  if (group.length === 0) continue;
  const percent = Math.round(group.length / tasks.length * 100);
  const bar = "█".repeat(Math.ceil(percent / 5)) + 
              "░".repeat(20 - Math.ceil(percent / 5));
  dv.paragraph(`${label} \`${bar}\` ${group.length} (${percent}%)`);
}

// 总进度
const allTasks = dv.pages('"tasks"').file.tasks;
const completed = allTasks.where(t => t.completed).length;
const total = allTasks.length;
const overallPercent = total > 0 ? Math.round(completed / total * 100) : 0;

dv.header(3, `📊 总进度: ${overallPercent}%`);
const filledBlocks = Math.floor(overallPercent / 2);
const progressBar = "▓".repeat(filledBlocks) + 
                    "░".repeat(50 - filledBlocks);
dv.paragraph(`\`${progressBar}\` ${completed}/${total}`);

3. 笔记热力图

dataviewjs
// 生成类似 GitHub 贡献图的热力图
const dailyNotes = dv.pages('"daily"');
const today = dv.date("today");
const startDate = today.minus({ days: 90 });

// 按日期分组
const noteCounts = {};
for (let p of dailyNotes) {
  if (p.file.day) {
    const dateStr = p.file.day.toFormat("yyyy-MM-dd");
    noteCounts[dateStr] = (noteCounts[dateStr] || 0) + 1;
  }
}

dv.header(3, "📈 最近 90 天笔记热力图");

const container = dv.el('div', '');
const grid = container.createEl('div', {
  attr: { 
    style: 'display: grid; grid-template-columns: repeat(15, 1fr); gap: 3px; max-width: 600px;'
  }
});

for (let d = 0; d < 90; d++) {
  const date = startDate.plus({ days: d });
  const dateStr = date.toFormat("yyyy-MM-dd");
  const count = noteCounts[dateStr] || 0;
  
  // 热度等级 0-4
  let level = 0;
  if (count >= 5) level = 4;
  else if (count >= 3) level = 3;
  else if (count >= 2) level = 2;
  else if (count >= 1) level = 1;
  
  const colors = [
    'var(--background-secondary)',
    '#9be9a8',
    '#40c463',
    '#30a14e',
    '#216e39'
  ];
  
  const cell = grid.createEl('div', {
    attr: {
      title: `${dateStr}: ${count} 篇`,
      style: `width: 100%; aspect-ratio: 1; border-radius: 2px; background: ${colors[level]};`
    }
  });
}

// 图例
const legend = container.createEl('div', {
  attr: { style: 'margin-top: 8px; display: flex; align-items: center; gap: 4px; font-size: 0.8em; color: var(--text-muted);' }
});
legend.createEl('span', { text: '少' });
for (let c of ['var(--background-secondary)', '#9be9a8', '#40c463', '#30a14e', '#216e39']) {
  legend.createEl('div', {
    attr: { style: `width: 12px; height: 12px; border-radius: 2px; background: ${c};` }
  });
}
legend.createEl('span', { text: '多' });

4. 读书统计报告

dataviewjs
const books = dv.pages('"books"')
  .where(p => p.author);

dv.header(3, "📚 阅读统计报告");

// 基本统计
const total = books.length;
const finished = books.where(p => p.status === "read").length;
const reading = books.where(p => p.status === "reading").length;
const toRead = books.where(p => p.status === "to-read").length;

dv.table(
  ["统计项", "数量"],
  [
    ["📚 总藏书量", total],
    ["✅ 已读完", finished],
    ["📖 阅读中", reading],
    ["📋 待阅读", toRead],
    ["📊 完读率", total > 0 ? `${Math.round(finished / total * 100)}%` : "—"]
  ]
);

// 按作者统计
const authorStats = {};
for (let b of books.where(p => p.status === "read")) {
  if (b.author) {
    authorStats[b.author] = (authorStats[b.author] || 0) + 1;
  }
}

const topAuthors = Object.entries(authorStats)
  .sort((a, b) => b[1] - a[1])
  .slice(0, 5);

if (topAuthors.length > 0) {
  dv.header(4, "🏆 最多产作者 Top 5");
  dv.table(
    ["作者", "已读书数"],
    topAuthors
  );
}

// 按评分排序
const rated = books.where(p => p.rating)
  .sort(p => -p.rating)
  .limit(10);

if (rated.length > 0) {
  dv.header(4, "⭐ 评分最高 Top 10");
  dv.table(
    ["书名", "作者", "评分", "读完日期"],
    rated.map(p => [
      p.file.link,
      p.author || "—",
      "⭐".repeat(p.rating),
      p.finished_date || "—"
    ])
  );
}

5. 自动化周报生成

dataviewjs
const today = dv.date("today");
const weekStart = today.minus({ days: today.weekday - 1 });
const weekEnd = weekStart.plus({ days: 6 });

dv.header(2, `📋 周报 ${weekStart.toFormat("yyyy-MM-dd")} ~ ${weekEnd.toFormat("yyyy-MM-dd")}`);

// 本周新增笔记
const newNotes = dv.pages()
  .where(p => p.file.ctime >= weekStart && p.file.ctime <= weekEnd.plus({ days: 1 }))
  .sort(p => p.file.ctime, 'desc');

dv.header(3, `📝 本周新增笔记 (${newNotes.length})`);
if (newNotes.length > 0) {
  dv.table(
    ["笔记", "创建时间", "分类"],
    newNotes.map(p => [
      p.file.link,
      p.file.ctime.toFormat("MM-dd HH:mm"),
      p.category || p.tags?.[0] || "—"
    ])
  );
}

// 本周修改笔记
const modifiedNotes = dv.pages()
  .where(p => p.file.mtime >= weekStart && p.file.mtime <= weekEnd.plus({ days: 1 }) 
    && p.file.ctime < weekStart)
  .sort(p => p.file.mtime, 'desc');

dv.header(3, `✏️ 本周修改笔记 (${modifiedNotes.length})`);
if (modifiedNotes.length > 0) {
  dv.list(modifiedNotes.limit(10).map(p => 
    `${p.file.link} — ${p.file.mtime.toFormat("MM-dd")}`
  ));
}

// 本周完成任务
const tasks = dv.pages().file.tasks
  .where(t => t.completed && t.completion >= weekStart && t.completion <= weekEnd.plus({ days: 1 }));

dv.header(3, `✅ 本周完成任务 (${tasks.length})`);
if (tasks.length > 0) {
  dv.taskList(tasks, false);
}

// 本周待办
const upcomingTasks = dv.pages().file.tasks
  .where(t => !t.completed)
  .sort(t => t.text);

dv.header(3, `📋 待办任务 (${upcomingTasks.length})`);
dv.taskList(upcomingTasks.limit(15), false);

性能优化

避免全库扫描

javascript
// ❌ 慢:扫描所有页面
const all = dv.pages().where(p => p.file.folder === "projects");

// ✅ 快:限定路径范围
const projects = dv.pages('"projects"');

// ✅ 更快:限定路径 + 过滤条件组合
const active = dv.pages('"projects"').where(p => p.status === "active");

使用 limit 减少处理量

javascript
// ✅ 先过滤再排序再限制
const recent = dv.pages('"daily"')
  .sort(p => p.file.day, 'desc')
  .limit(30);

// ❌ 避免:处理所有数据后再 slice
const all = dv.pages('"daily"').sort(p => p.file.day, 'desc');
const recent = all.slice(0, 30); // 已经处理了全部数据

缓存计算结果

dataviewjs
// 使用 dataviewjs 的缓存机制
const cache = dv.current().cache || {};
if (!cache.stats) {
  cache.stats = {
    total: dv.pages().length,
    computed: dv.date("today").toFormat("yyyy-MM-dd")
  };
}
dv.paragraph(`总笔记数:${cache.stats.total}(计算于 ${cache.stats.computed})`);

注意事项

DataviewJS 安全提示

  • DataviewJS 会执行任意 JavaScript 代码,仅在你信任的笔记中启用
  • 不要在共享仓库中使用未经验证的 DataviewJS 代码
  • 复杂查询可能影响性能,建议使用 limit() 控制结果数量
  • DataviewJS 代码在每次渲染时执行,避免在循环中做重复计算

相关文档