添加文档或块到指定数据库(支持绑定块和不绑定块)

see https://ld246.com/article/1746153210116

// 添加块到指定数据库(支持绑定块和不绑定块,支持文档块和普通块)
// see https://ld246.com/article/1746153210116
// 注意:只能在块菜单中操作(你的右键可能不是块菜单)
// version 0.0.3
// 0.0.2 (已废弃)
// 0.0.3 修改参数配置方式
(()=>{
    // 块菜单配置
    const menus = [
        {
            // 菜单名,显示在块或文档右键菜单上
            name: "添加到数据库A",
            // 添加到的数据库块id列表(必填),注意是数据库所在块id,如果移动了数据库位置需要更改
            toAvBlockId: "20250501223635-2hu6d9z",
            // 指定数据库的列名,不填默认是添加到主键列,该参数仅对不绑定块菜单有效,如果多个列名一样的则取第一个
            // 注意,目前仅支持文本列
            toAvColName: "",
            // 是否绑定块菜单,true 绑定,false 不绑定
            isBindBlock: true,
        },
        {
            name: "添加到数据库B",
            toAvBlockId: "20250502120433-a7o0ahx",
            toAvColName: "",
            isBindBlock: false,
        },
        {
            name: "添加到数据库C",
            toAvBlockId: "20250502120433-a7o0666",
            toAvColName: "",
            isBindBlock: false,
        }
    ];
    
    // 监听块右键菜单
    whenElementExist('#commonMenu .b3-menu__items').then((menuItems) => {
        const menusReverse = menus.reverse();
        observeBlockMenu(menuItems, async (isTitleMenu)=>{
            if(menuItems.querySelector('.add-to-my-av')) return;
            const addAv = menuItems.querySelector('button[data-id="addToDatabase"]');
            if(!addAv) return;
            if(menus.length === 0) return;
            // 生成块菜单
            menusReverse.forEach((menu,index) => {
                const menuText = menu.name+ (menu.isBindBlock?'':'(不绑定块)');
                const menuIcon = '#iconDatabase';
                const menuClass = `add-to-my-av-${menu.toAvBlockId}-${menus.length-index-1}`;
                const menuButtonHtml = `<button class="b3-menu__item ${menuClass}"><svg class="b3-menu__icon " style=""><use xlink:href="${menuIcon}"></use></svg><span class="b3-menu__label">${menuText}</span></button>`;
                addAv.insertAdjacentHTML('afterend', menuButtonHtml);
                const menuBtn = menuItems.querySelector('.'+menuClass);
                // 块菜单点击事件
                menuBtn.onclick = async () => {
                    window.siyuan.menus.menu.remove();
                    menuItemClick(menu.toAvBlockId, menu.toAvColName, menu.isBindBlock, isTitleMenu);
                };
            });
        });
    });
    // 菜单点击事件
    async function menuItemClick(toAvBlockId, toAvColName, isBindBlock, isTitleMenu) {
        const avId = await getAvIdByAvBlockId(toAvBlockId);
        if(!avId) {
            showMessage('未找到块ID'+toAvBlockId+'所在的数据库,请检查数据库块ID配置是否正确', true);
            return;
        }
        let blocks = [];
        if(isTitleMenu) {
            // 添加文档块到数据库
            const docTitleEl = (document.querySelector('[data-type="wnd"].layout__wnd--active .protyle:not(.fn__none)')||document.querySelector('[data-type="wnd"] .protyle:not(.fn__none)'))?.querySelector('.protyle-title');
            const docId = docTitleEl?.dataset?.nodeId;
            const docTitle = docTitleEl?.querySelector('.protyle-title__input')?.textContent;
            blocks = [{
                dataset: {nodeId: docId},
                textContent: docTitle,
            }];
        } else {
            // 添加普通块到数据库
            blocks = document.querySelectorAll('.protyle-wysiwyg--select');
        }
        // 绑定块
        if(isBindBlock){
            const blockIds = [...blocks].map(block => block.dataset.nodeId);
            addBlocksToAv(blockIds, avId, toAvBlockId);
        }
        // 非绑定块
        else {
            // 通过字段名获取keyID
            const keys = await requestApi("/api/av/getAttributeViewKeysByAvID", {avID:avId});
            // 获取主键id
            let pkKeyID = keys?.data[0]?.id || '';
            if(!pkKeyID) {
                pkKeyID = keys?.data?.find(item=>item.type === 'block')?.id;
            }
            // 获取指定字段id
            let keyID = '';
            if(toAvColName) {
                keyID = keys?.data?.find(item=>item.name === toAvColName.trim())?.id;
            }
            addBlocksToAvNoBind(blocks, avId, pkKeyID, keyID);
        }
    }
    // 通过块id获取数据库id
    async function getAvIdByAvBlockId(blockId) {
        const av = await getAvBySql(`SELECT * FROM blocks where type ='av' and id='${blockId}'`);
        if(av.length === 0) return '';
        const avId = av.map(av => getDataAvIdFromHtml(av.markdown))[0];
        return avId || '';
    }
    // 从数据库HTML代码中获取数据库id
    function getDataAvIdFromHtml(htmlString) {
        // 使用正则表达式匹配data-av-id的值
        const match = htmlString.match(/data-av-id="([^"]+)"/);
        if (match && match[1]) {
        return match[1];  // 返回匹配的值
        }
        return "";  // 如果没有找到匹配项,则返回空
    }
    // 通过sql获取数据库信息
    async function getAvBySql(sql) {
        const result = await requestApi('/api/query/sql', {"stmt": sql});
        if(result.code !== 0){
            console.error("查询数据库出错", result.msg);
            return [];
        }
        return result.data;
    }
    // 插入块到数据库
    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 requestApi('/api/av/addAttributeViewBlocks', input);
        if(!result || result.code !== 0) console.error(result);
    }

    // 插入块到数据库(非绑定)
    async function addBlocksToAvNoBind(blocks, avId, pkKeyID, keyID) {
        const values = [...blocks].map(block => {
            // 必须添加主键列
            const rowValues = [{
                "keyID": pkKeyID,
                "block": {
                  "content": keyID ? "" : block.textContent
                }
            }];
            if(keyID) {
                rowValues.push({
                    "keyID": keyID,
                    "text": {
                      "content": block.textContent
                    }
                });
            }
            return rowValues;
        });
        const input = {
          "avID": avId,
          "blocksValues": values,
        }
        const result = await requestApi('/api/av/appendAttributeViewDetachedBlocksWithValues', input);
        if(!result || result.code !== 0) console.error(result);
    }

    // 请求api
    async function requestApi(url, data, method = 'POST') {
        return await (await fetch(url, {method: method, body: JSON.stringify(data||{})})).json();
    }

    /**
     * 监控 body 直接子元素中 #commonMenu 的添加
     * @returns {MutationObserver} 返回 MutationObserver 实例,便于后续断开监听
     */
    function observeBlockMenu(selector, callback) {
        let hasFlag1 = false;
        let hasFlag2 = false;
        let isTitleMenu = false;
        // 创建一个 MutationObserver 实例
        const observer = new MutationObserver((mutationsList) => {
            // 遍历所有变化
            for (const mutation of mutationsList) {
                // 检查是否有节点被添加
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // 遍历所有添加的节点
                    mutation.addedNodes.forEach((node) => {
                        // 检查节点是否是目标菜单
                        if((hasFlag1 && hasFlag2) || isTitleMenu) return;
                        if (node.nodeType === 1 && node.querySelector('.b3-menu__label')?.textContent?.trim() === window.siyuan.languages.cut) {
                            hasFlag1 = true;
                        }
                        if (node.nodeType === 1 && node.querySelector('.b3-menu__label')?.textContent?.trim() === window.siyuan.languages.move) {
                            hasFlag2 = true;
                        }
                        if(node.nodeType === 1 && node.closest('[data-name="titleMenu"]')) {
                            isTitleMenu = true;
                        }
                        if((hasFlag1 && hasFlag2) || isTitleMenu) {
                           callback(isTitleMenu);
                           setTimeout(() => {
                               hasFlag1 = false;
                               hasFlag2 = false;
                               isTitleMenu = false;
                           }, 200);
                        }
                    });
                }
            }
        });
    
        // 开始观察 body 的直接子元素的变化
        observer.observe(selector || document.body, {
            childList: true, // 监听子节点的添加
            subtree: false, // 仅监听直接子元素,不监听子孙元素
        });
    
        // 返回 observer 实例,便于后续断开监听
        return observer;
    }

    // 等待元素出现
    function whenElementExist(selector, node) {
        return new Promise(resolve => {
            const check = () => {
                const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
                if (el) resolve(el); else requestAnimationFrame(check);
            };
            check();
        });
    }

    function showMessage(message, isError = false, delay = 7000) {
        return fetch('/api/notification/' + (isError ? 'pushErrMsg' : 'pushMsg'), {
            "method": "POST",
            "body": JSON.stringify({"msg": message, "timeout": delay})
        });
    }
})();

image.png

留下你的脚步
推荐阅读