我想用dataview做一个简单的每周工作时间统计

请仔细说明自己遇到的问题,以下是参考模板。这里不要求非得按模板发帖,但内容中包含相关要素能让大家更好地帮助你。


遇到的问题

我想要用dataview做一个简单的每周工作时间统计,非常简单:
我会在每天的日记中记录今天“工作了几个小时”,“游戏玩了几个小时”,“视频看了几个小时”,“其他事项几个小时”
我会使用元数据,也就是:[[work: 1]][[game: 0.15]][[video: 0.52]]的格式
然后我希望使用dataview配合模板,每周生成一个“每周工作几个小时”,“游戏玩了几个小时”等内容的dataview查询文档,这样每周就能看到时间度过的情况
日记放在Dailynotes文件夹中,日记文件名格式与位置为Dailynotes/yyyy/MM/yyyy-MM-DD

预期的效果

我希望它能表现成下面这个样子:这个例子来自https://zhuanlan.zhihu.com/p/700968386, 表格列求和那段


我不指望完全一样,能满足需求就可以

已尝试的解决方案

我觉得这个应用应该够基础了,但是我转了一圈,没有完美符合要求的。我跟chatgpt和deepseek都问过了,他们给的代码都跑不了,我只能说向人类问问题不丢人。

我感觉最关键的一个坑是 dv 的 inline 属性应该是 [work:: 1] [game:: 0.15] [video:: 0.52]

而不是 [[work: 1]] [[game: 0.15]] [[video: 0.52]] 后者的写法不常见, 需要专门解析, AI 估计不知道怎么抄现成代码

如果改成标准的 dv inline 属性写法, AI 还是基本能写成的
以下绝大部分是 AI 生成的 (也有报错, 原样把报错贴回去让它改), 知识库就只参考了楼主链接里的达人系列前三篇


```dataviewjs
// 初始化统计变量
let totalWork = 0;
let totalGame = 0;
let totalVideo = 0;
let totalOther = 0;

// 初始化数据数组
const data = []; // 确保 data 是一个数组

// 遍历指定文件夹中的所有文件
const pages = dv.pages('"来源日记的路径"').where(page => page.work).sort(p => p.file.name);
// 创建表格数据
const headers = ["日期", "工作", "游戏", "视频", "其他"];
pages.forEach(page => {
  totalWork += page.work || 0;
  totalGame += page.game || 0;
  totalVideo += page.video || 0;
  totalOther += page.other || 0;
  data.push([
    page.file.day, // 日期
    page.work || 0, // 工作时间
    page.game || 0, // 游戏时间
    page.video || 0, // 视频时间
    page.other || 0 // 其他时间
  ]);
});

// 添加总计行
data.push([
  "总计",
  totalWork,
  totalGame,
  totalVideo,
  totalOther
]);

// 渲染表格
dv.table(headers, data);
```

这个倒是能跑,但是我希望它是按周来计算的,我希望他能自动计算一周内的内容而不是指定
我把能找到的示例文档都喂给AI了,结果它还是不行……看来没有符合逻辑的实例,它就生成不了
实在不行我就只能把日记的格式变成以周进行计算的了,这样至少路径就确定了

实在不行我就只能把日记的格式变成以周进行计算的了

不用啊, 接着上面代码继续完善就行了呗
每天数据都有了, 弄个每周报表还不是轻轻松松的事, 且这时不涉及 dv 特殊语法了, 全都是 js 公用的语法, AI 基本不会写错

完工了,感谢支持,我让AI用最直接的指定文件路径的方式来做,它终于成功了。AI喜欢乱使用未定义的函数,他只是单纯的觉得这个逻辑用在这段代码里很合适,不考虑代码本身究竟能不能用,最后可能还是要人类来出主意

// 初始化统计变量
let totalWork = 0;
let totalGame = 0;
let totalVideo = 0;
let totalOther = 0;
const data = [];
// 修改后的文件指定方式(手动添加7天文件路径)
const weekFiles = [
    "DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 0) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 1) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 2) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 3) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 4) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 5) %>",
	"DailyNotes/<% tp.date.now("YYYY") %>/<% tp.date.now("MM") %>/<% tp.date.weekday("YYYY-MM-DD", 6) %>",
];

// 修改后的核心代码行
const pages = dv.pages(weekFiles.map(f => `"${f}"`).join(" OR ")).sort(p => p.file.name);

// 构建表格数据(保持原逻辑不变)
pages.forEach(page => {
    totalWork += page.work || 0;
    totalGame += page.game || 0;
    totalVideo += page.video || 0;
    totalOther += page.other || 0;
    
    data.push([
        dv.date(page.file.day).toFormat("yyyy-MM-dd"),
        page.work || 0,
        page.game || 0,
        page.video || 0,
        page.other || 0
    ]);
});

// 添加总计行
data.push(["总计", totalWork, totalGame, totalVideo, totalOther]);

// 渲染表格
dv.table(["日期", "工作", "游戏", "视频", "其他"], data);

2 个赞

这个 2小时、2分 中间的顿号可以去掉吗?
强迫症不能忍。

实现功能一览:

在日记中的记录:

image

在月记中的统计实现:

说明:

  • 标签列是一级标签,明细列是一级标签下的子标签,只支持两级标签。
  • 时长统计根据日记中的记录,自动逐行做差得到。

实现:

所需插件:

  • QucikAdd
  • DataView

日记中的实现:

  1. 指定一个节名,如图1中的"Time Log",这对应代码中的:l.section?.subpath === "Time Log"

  2. 手动记录每次任务开始时的时间,并加上标签,记录格式为- hh:mm do what #tag

    • 每次手动输入时间是很麻烦的,可以利用QuickAdd插件制作模板,实现使用快捷键在日记中的指定位置插入当前时间以及你想记录的内容,这一小部分的教程请参考:少数派-当 Obsidian 与时间管理相遇 该教程中有两个错误:1. 创建 QuickAdd 的时候,选择 capture 而不是截图中的 template 2. capture format value 是小写。大写的VALUE 无法捕获。
    • 注意最后一行需要有个不带标签的结束,例如图1中的"END",这样才能完整做差,计算时间。
  3. 需要在日记模板中加入日期与本年第几周的属性:
    image

    • 属性的代码实现如下:
      ---
      date: <% tp.file.creation_date("YYYY-MM-DD") %>
      NO.week: <% window.moment().format("GGGG-[W]WW") %>
      ---

月记中的实现:

  • 月记名为"YYYY-MM"格式,不是的话代码无法自动提取,需要你在代码中自行指定月份。
  • 代码如下,注释挺良好的,记得改下文件夹路径之类的,其他看不懂的,GPT都懂:
    /***********************
     * 工具函数
     ***********************/
    function parseTime(t) {
      const [h, m] = t.split(":").map(Number);
      return h * 60 + m;
    }
    
    function getMonth(day) {
      return `${day.year}-${String(day.month).padStart(2, "0")}`;
    }
    
    /***********************
     * 当前月(从月记文件名取)
     ***********************/
    const currentMonth = dv.current().file.name.match(/\d{4}-\d{2}/)?.[0];
    
    /***********************
     * 数据结构,不用管,自动填入
     ***********************/
    // daily[date][root] = { total, children }
    // weekly[week][root] = { total, children }
    // monthly[root] = { total, children }
    
    let daily = {};
    let weekly = {};
    let monthly = {};
    
    /***********************
     * 只有在能识别月份时才执行统计
     ***********************/
    if (currentMonth) {
    
      /***********************
       * 扫描本月所有日记,在“日记”文件夹下
       ***********************/
      for (const page of dv.pages('"日记"')) {
    
        if (!page.date) continue;
    
        const pageDate = dv.date(page.date);
        if (getMonth(pageDate) !== currentMonth) continue;
    
        const dateKey = dv.date(page.date).toFormat("yyyy-MM-dd");
        const weekKey = page["NO.week"] ?? "Unknown";
    
        daily[dateKey] ??= {};
        weekly[weekKey] ??= {};
    
        const logs = page.file.lists.filter(
          l => l.section?.subpath === "Time Log" //Time Log是你自定义的节名
        );
    
        for (let i = 0; i < logs.length - 1; i++) {
    
          const curr = logs[i].text;
          const next = logs[i + 1].text;
    
          const t1 = curr.match(/\d{2}:\d{2}/)?.[0];
          const t2 = next.match(/\d{2}:\d{2}/)?.[0];
          if (!t1 || !t2) continue;
    
          const hours = (parseTime(t2) - parseTime(t1)) / 60;
          if (hours <= 0) continue;
    
          const tags = curr.match(/#\S+/g) ?? [];
    
          for (const tag of tags) {
    
            const parts = tag.slice(1).split("/");
            const root = "#" + parts[0];
            const child = parts.length > 1 ? parts.slice(1).join("/") : null;
    
            // 向 daily / weekly / monthly 三个桶同时累加
            for (const bucket of [
              daily[dateKey],
              weekly[weekKey],
              monthly
            ]) {
              bucket[root] ??= { total: 0, children: {} };
              bucket[root].total += hours;
    
              if (child) {
                bucket[root].children[child] =
                  (bucket[root].children[child] ?? 0) + hours;
              }
            }
          }
        }
      }
    
      /***********************
       * 输出工具
       ***********************/
    	function renderTable(title, data, firstCol, withSubtotal = false) {
    	  dv.header(3, title);
    	
    	  dv.table(
    	    [firstCol, "标签", "总时长", "明细"],
    	    Object.entries(data)
    	      // 排序(weekly / monthly 都安全)
    	      .sort(([a], [b]) => dv.date(a) - dv.date(b))
    	      .flatMap(([key, roots]) => {
    	
    	        const rows = [];
    	        let subtotal = 0;
    	
    	        const entries = Object.entries(roots);
    	
    	        entries.forEach(([root, info], idx) => {
    	          subtotal += info.total;
    	
    	          const childText = Object.entries(info.children)
    	            .sort((a, b) => b[1] - a[1])
    	            .map(([k, v]) => `${k}: ${v.toFixed(1)}h`) //toFixed中的"1"是小数点后1位
    	            .join("<br>");
    	
    	          rows.push([
    	            idx === 0 ? key : "",
    	            root,
    	            info.total.toFixed(1) + "h",
    	            childText || "<span style='color: gray;'>无</span>"
    	          ]);
    	        });
    	
    	        // ⭐ 小计行(只在 weekly / monthly 开启)
    	        if (withSubtotal && entries.length > 0) {
    	          rows.push([
    	            "",
    	            "小计",
    	            `${subtotal.toFixed(1)}h`,
    	            ""
    	          ]);
    	        }
    	
    	        return rows;
    	      })
    	  );
    	}
    
      /***********************
       * 三张表
       ***********************/
      renderTable("📊 本月统计", { "本月": monthly }, "范围", true);
      renderTable("🗓️ 每周统计", weekly, "周", true);
      renderTable("⏳ 逐天分布", daily, "日期", false);
    
    } else {
      dv.paragraph("❌ 月记文件名中未找到 YYYY-MM");
    }
    

其他:

  • 可结合Charts 插件做一些更精细化的分析,实现可请教GPT大人。
  • 推荐了解:插件-worklogger实现了QuickAdd的自动记录和月记中自动统计时长的功能,界面优雅,支持接入大模型编写周报等。但对我来说,封装度有点高,我不想为了time log单独建立一个文件夹。