配合标签和数据库模板列实现日记数据库的动态管理

想要实现的功能

通过给日记文档添加固定标签的方式,将日记自动添加进数据库中,并使用模板列自动获取日记中的指定内容。配合叶归插件实现基于每天的流式日记。

具体实现方式

首先添加一段 js 代码实现对标签和指定数据库的匹配,具体详见任务管理数据库模板分享,以下是 js 代码片段。

// 思源通过标签插入当前块到数据库(SuperTag)
// 功能:给块设置标签,将块添加到标签同名的数据库。
// 说明;
// 1、数据库名称需要与标签同名,名称需含前后#,如 #笔记软件#
// 2、如果有多个同名数据库,只会将块添加到其中一个,所以不要建立同名数据库
// 3、需要提前建立数据库才能添加成功 
// version:0.0.4
// 更新记录
// 0.0.2 增加数据库同名文档标签即可把文档添加到标签同名数据库中(文档标签这里指文档头部的添加标签)。
// 0.0.3 增加可删除数据库引用文本中的标签名选项isShowTagNameInAvCell
// 0.0.4 改进当标签在列表中时数据库插入列表项
// 根据qiancang大佬的帖子实现 https://ld246.com/article/1731945645865
(()=>{
    // 添加tag后多少毫秒添加当前块到数据库
    // 不宜设置过小,过小可能导致标签被插入一半
    const delay = 1000;

    // 是否开启文档标签插入同名数据库(文档标签这里指文档头部的添加标签),true开启,false不开启
    const enableDocTagToAv = true;

    // 是否在数据库列表中显示标签名(数据库名),true显示,false不显示(注意,文档块引用不会添加标签名,实现较麻烦暂不支持)
    const isShowTagNameInAvCell = false;
  
    // 发布服务立即返回
    if(siyuan.config.readonly) return;
  
    // 监听tag输入
    observeTagSpans(async (tagEl, tagType) => {
        // 如果未开启文档标签插入同名数据库,当为文档标签时返回
        if(!enableDocTagToAv && tagType === 'doc-tag') return;
        // 去掉零宽度字符&ZeroWithSpace;
        const tag = tagEl?.textContent?.replace(/[\u200B-\u200D\uFEFF]/g, '')?.trim();
        if(!tag) return;
        // 获取数据库信息
        const av = await getAvByName(tag);
        if(!av) return;
        const avId = av.avID;
        if(!avId) return;
        const avBlockID = av.blockID;
        if(!avBlockID) return;
        // 获取文档块信息
        let block;
        if(tagType === 'doc-tag') {
            // 如果头部标签,返回文档id
            const blockParent = tagEl.closest('div.protyle-top');
            if(!blockParent) return;
            block = blockParent.querySelector('.protyle-title');
        } else {
            // 如果块标签,返回块id(监听元素的临时块)
            block = tagEl.closest('div[data-node-id][data-type]');
            if(!block) return;
            // 获取文档中的block结点
            block = document.querySelector('div[data-node-id="'+(block?.dataset?.nodeId||'')+'"]');
            if(!block) return;
            // 判断是否在列表元素内,数据库插入列表项
            const listItemNode = block.closest('div[data-node-id][data-type="NodeListItem"]');
            if(listItemNode) block = listItemNode;
        }
        const blockId = block?.dataset?.nodeId;
        if(!blockId) return;
        // 添加块到数据库
        await sleep(delay || 500);
        addBlocksToAv(blockId, avId, avBlockID);
    });

    // 如果不在数据库中显示标签名则删除标签名(注意,文档块引用不会添加标签名,实现较麻烦暂不支持)
    if(!isShowTagNameInAvCell) {
        observeElementCreation(
            document.body,
            '.av__row:not(.av__row--header) [data-dtype="block"] [data-type="block-ref"]',
            async ref => {
                if(!/\s?#.*?#/i.test(ref.textContent)) return;
                ref.textContent = ref.textContent.replace(/\s?#.*?#/ig, '');
            }
        );
    }
  
    // 插入块到数据库
    async function addBlocksToAv(blockIds, avId, avBlockID) {
        blockIds = typeof blockIds === 'string' ? [blockIds] : blockIds;
        const srcs = blockIds.map(blockId => ({
            "id": blockId,
            "isDetached": false,
        }));
        const input = {
          "avID": avId,
          "blockID": avBlockID,
          'srcs': srcs
        }
        const result = await fetchSyncPost('/api/av/addAttributeViewBlocks', input);
        if(!result || result.code !== 0) console.error(result);
    }
    // 通过该tag查询数据库
    async function getAvByName(name) {
        const result = await fetchSyncPost('/api/av/searchAttributeView', {
            "keyword": name
        });
        if(!result || result.code !== 0 || !result?.data?.results || result?.data?.results?.length === 0) return null;
        for (const av of result.data.results) {
            if (av.avName === name || av.avName === `#${name}#`) {
                return av;
            }
        }
        return null;
    }
    // 请求api
    // returnType json返回json格式,text返回文本格式
    async function fetchSyncPost(url, data, returnType = 'json') {
        const init = {
            method: "POST",
        };
        if (data) {
            if (data instanceof FormData) {
                init.body = data;
            } else {
                init.body = JSON.stringify(data);
            }
        }
        try {
            const res = await fetch(url, init);
            const res2 = returnType === 'json' ? await res.json() : await res.text();
            return res2;
        } catch(e) {
            console.log(e);
            return returnType === 'json' ? {code:e.code||1, msg: e.message||"", data: null} : "";
        }
    }
    // 延迟执行
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    // 监听tag被添加
    function observeTagSpans(callback) {
        // 创建一个观察者实例并传入回调函数
        const observer = new MutationObserver((mutationsList, observer) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    // 检查新增的节点
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'span' && node.getAttribute('data-type') === 'tag') {
                            // 块标签调用回调函数
                            callback(node, 'block-tag');
                        } else if(node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'div' && node.classList?.contains('b3-chip') && node.getAttribute('data-type') === 'open-search') {
                            // 文档头部标签调用回调函数
                            callback(node, 'doc-tag');
                        }
                    }
                }
            }
        });
        // 配置观察选项:
        const config = { 
            childList: true, // 观察子节点的变化(添加/删除)
            subtree: true // 观察所有后代节点
        };
        // 选择需要观察变动的节点
        const targetNode = document.body; // 或者选择更具体的父节点以减少性能消耗
        // 开始观察目标节点
        observer.observe(targetNode, config);
        // 返回一个取消观察的方法
        return () => observer.disconnect();
    }
    // 监听元素被创建
    function observeElementCreation(parentNode, selector, onElementCreated) {
        // 配置观察器选项
        const config = { 
            childList: true, // 观察直接子节点的添加和移除
            subtree: true    // 观察所有后代节点
        };
        // 当检测到变动时执行的回调函数
        const callback = function(mutationsList, observer) {
            for (let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // 使用 querySelectorAll 查找所有符合条件的新元素
                            const elements = node.querySelectorAll(selector);
                            elements.forEach(element => {
                                onElementCreated(element); // 调用外部提供的回调函数
                            });
                        }
                    });
                }
            }
        };
        // 创建一个观察器实例并传入回调函数
        const observer = new MutationObserver(callback);
        // 开始观察目标节点
        observer.observe(parentNode, config);
        // 返回一个函数来停止观察
        return () => observer.disconnect();
    }
})();

然后新建日记,通过给日记添加标签的形式,将每天的日记文档的标题自动添加进对应的日记数据库中。比如我给我每天的日记添加 < 日记 > 标签,我相对应的日记数据库就会自动添加进每天新建的日记文档。

image

然后根据数据库的主键(在这里就是日记文档的标题),获取文档的 root_id 然后通过模板列检索文档中不同标签下的内容。比如我的日记中我设置了位置、天气、评分、随笔感悟以及今日总结五个标签,我在每个标签下填入相应的内容,然后在数据库中就可以通过对文档中指定标签的搜索,获取标签下的全部内容,下面是代码(将“随笔感悟”替换为自己的标签名称)

<获取对应文档快>
.action{ $blocks := queryBlocks "select * from blocks where root_id in (select root_id from blocks where id='?') and content like '%随笔感悟%'" .id}
<获取对应文档中制定标签下的所有内容>
.action{ if not (empty $blocks)}
    .action{range $block := $blocks}
        .action{trimall "#随笔感悟" $block.Content}
    .action{end}
.action{ else }
    
.action{ end }

这段代码参考了思源模板功能新人指南:模板语法 + 函数 + md 块语法的示例,然后我基于自己的需求稍微修改了一下,其中 trimall 函数是为了将每个内容块前面的标签内容删除,具体可以参考字符串函数。实现效果:

image

除了主键以外,其他列的内容都是通过模板列自动获取的。

一些问题和思考

其实我感觉应该是可以通过搜索指定标题下的内容块的方式进行汇总,这样就不用在每个内容块前面设置标签,在记录内容的时候能够更加流畅一些,我在实践】基于 query 实现的项目信息管理 + 面板视图这篇帖子看到通过 query 实现了这样的形式,我尝试了将帖子中的代码迁移到思源的模板函数中,但是奈何自己完全没有写代码的经验,没有成功,就放弃了 😂。因为添加标签对我来说也不是一件很麻烦的事情,所以就没有纠结。不知道有没有大佬能够帮帮忙,提供代码让我学习一下 😊

对了,后面还刷到了一种实现方也挺好的:数据库一些列的值能否自动获取文档里的数据,我因为想要记录 lifelog 所以这个方法对于我来说就不太适合。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119783 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...