飞书文档批量替换

飞书在线文档World,Excel批量替换

// ==UserScript==
// @name         飞书文档批量替换
// @namespace    https://www.gd520.cc
// @version      2025-02-19
// @description  飞书在线文档World,Excel批量替换
// @author       Guodong
// @license      GPL
// @match        https://weboffice.feishu-3rd-party-services.com/office/s/*
// @match        https://weboffice.feishu-3rd-party-services.com/office/w/*
// @icon         https://www.feishu.cn/favicon.ico
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // 如果 URL 中包含 require_mode=1,则退出脚本
    if (window.location.href.includes('require_mode=1')) {
        console.log('已检测到 require_mode=1,脚本不执行');
        return;
    }

    const excelDoc = window.location.href.includes('/office/s/');

    function setInputValue(input, value) {
        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        nativeInputValueSetter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
    }

    let debounceTimeout;
    function debounceSave(fn, delay) {
        clearTimeout(debounceTimeout);
        debounceTimeout = setTimeout(fn, delay);
    }

    function createReplacePanel() {
        const panel = document.createElement('div');
        Object.assign(panel.style, {
            position: 'fixed',
            bottom: '65px',
            right: '30px',
            width: '515px',
            maxHeight: '360px',
            overflowY: 'auto',
            backgroundColor: '#fff',
            border: '1px solid #ccc',
            borderRadius: '8px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
            zIndex: 10000,
            fontFamily: 'Arial, sans-serif',
            fontSize: '14px',
            color: '#333'
        });

        const showIconButton = document.createElement('button');
        showIconButton.innerHTML = '☰ 飞书文档助手'; 
        Object.assign(showIconButton.style, {
            position: 'fixed',
            bottom: '65px',
            right: '30px',
            display: 'none',
            cursor: 'pointer',
            zIndex: 10001
        });
        styleButton(showIconButton, '#795548');
        showIconButton.addEventListener('click', () => {
            panel.style.display = 'block';
            showIconButton.style.display = 'none';
        });
        document.body.appendChild(showIconButton);

        const headerDiv = document.createElement('div');
        Object.assign(headerDiv.style, {
            position: 'sticky',
            top: '0',
            backgroundColor: '#fff',
            padding: '15px 15px 0 15px'
        });
        panel.appendChild(headerDiv);

        const title = document.createElement('h2');
        title.textContent = '飞书文档批量替换 - Guodong';
        title.style.display = 'inline-block';
        title.style.marginBottom = '10px';
        headerDiv.appendChild(title);

        const hideButton = document.createElement('button');
        hideButton.textContent = '隐藏';
        styleButton(hideButton, '#E91E63');
        hideButton.style.float = 'right';
        hideButton.addEventListener('click', () => {
            panel.style.display = 'none';
            showIconButton.style.display = 'block';
        });
        headerDiv.appendChild(hideButton);

        const buttonContainer = document.createElement('div');
        buttonContainer.style.marginTop = '10px';
        headerDiv.appendChild(buttonContainer);

        const executeButton = document.createElement('button');
        executeButton.textContent = '执行';
        styleButton(executeButton, '#2196F3');
        buttonContainer.appendChild(executeButton);

        const addButton = document.createElement('button');
        addButton.textContent = '添加';
        styleButton(addButton, '#4CAF50');
        buttonContainer.appendChild(addButton);

        const exportButton = document.createElement('button');
        exportButton.textContent = '导出';
        styleButton(exportButton, '#FF9800');
        buttonContainer.appendChild(exportButton);

        const importButton = document.createElement('button');
        importButton.textContent = '导入';
        styleButton(importButton, '#9C27B0');
        buttonContainer.appendChild(importButton);

        const docType = document.createElement('h4');
        docType.textContent = excelDoc ? 'Excel' : 'Word';
        Object.assign(docType.style, {
            marginLeft: '10px',
            display: 'inline-block',
            marginBottom: '5px'
        });
        buttonContainer.appendChild(docType);

        const replaceListContainer = document.createElement('div');
        replaceListContainer.style.marginLeft = '10px';
        replaceListContainer.style.marginTop = '5px';
        panel.appendChild(replaceListContainer);

        let replacePairs = [];
        try {
            replacePairs = JSON.parse(localStorage.getItem('replacePairs')) || [];
        } catch (error) {
            console.log('读取规则失败,可能数据格式错误', error);
        }
        console.log('油猴:Loaded replace pairs from localStorage:', replacePairs);

        function updateReplaceList() {
            replaceListContainer.innerHTML = '';
            replacePairs.forEach((pair, index) => {
                const pairDiv = document.createElement('div');
                pairDiv.style.display = 'flex';
                pairDiv.style.alignItems = 'center';
                pairDiv.style.margin = '5px';

                const findInput = document.createElement('input');
                findInput.type = 'text';
                findInput.placeholder = '查找';
                findInput.value = pair.findText;
                styleInput(findInput);
                findInput.addEventListener('input', () => {
                    pair.findText = findInput.value;
                    debounceSave(saveReplacePairs, 300);
                });

                const replaceInput = document.createElement('input');
                replaceInput.type = 'text';
                replaceInput.placeholder = '替换';
                replaceInput.value = pair.replaceText;
                styleInput(replaceInput);
                replaceInput.addEventListener('input', () => {
                    pair.replaceText = replaceInput.value;
                    debounceSave(saveReplacePairs, 300);
                });

                const upButton = document.createElement('button');
                upButton.textContent = '↑';
                styleButton(upButton, '#795548');
                upButton.style.marginRight = '4px';
                upButton.addEventListener('click', () => {
                    if (index > 0) {
                        [replacePairs[index - 1], replacePairs[index]] = [replacePairs[index], replacePairs[index - 1]];
                        saveReplacePairs();
                        updateReplaceList();
                    }
                });

                const downButton = document.createElement('button');
                downButton.textContent = '↓';
                styleButton(downButton, '#795548');
                downButton.style.marginRight = '4px';
                downButton.addEventListener('click', () => {
                    if (index < replacePairs.length - 1) {
                        [replacePairs[index + 1], replacePairs[index]] = [replacePairs[index], replacePairs[index + 1]];
                        saveReplacePairs();
                        updateReplaceList();
                    }
                });

                const deleteButton = document.createElement('button');
                deleteButton.textContent = 'X';
                styleButton(deleteButton, '#f44336');
                deleteButton.addEventListener('click', () => {
                    replacePairs.splice(index, 1);
                    saveReplacePairs();
                    updateReplaceList();
                });

                pairDiv.appendChild(findInput);
                pairDiv.appendChild(replaceInput);
                pairDiv.appendChild(upButton);
                pairDiv.appendChild(downButton);
                pairDiv.appendChild(deleteButton);
                replaceListContainer.appendChild(pairDiv);
            });
        }

        function saveReplacePairs() {
            localStorage.setItem('replacePairs', JSON.stringify(replacePairs));
        }

        addButton.addEventListener('click', () => {
            replacePairs.push({ findText: '', replaceText: '' });
            updateReplaceList();
            saveReplacePairs();
        });

        executeButton.addEventListener('click', async () => {
            await performMultipleReplaces(replacePairs);
        });

        exportButton.addEventListener('click', () => {
            const now = new Date();
            const fileName = formatTime(now) + 'FeishuRule.txt';
            const blob = new Blob([JSON.stringify(replacePairs, null, 2)], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const link = document.createElement('a');
            link.href = url;
            link.download = fileName;
            link.click();
            URL.revokeObjectURL(url);
        });

        importButton.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.txt';
            input.onchange = (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = () => {
                    try {
                        const data = JSON.parse(reader.result);
                        if (Array.isArray(data)) {
                            replacePairs = data;
                            saveReplacePairs();
                            updateReplaceList();
                        }
                    } catch (err) {
                        console.log('导入规则时发生错误:', err);
                    }
                };
                reader.readAsText(file);
            };
            input.click();
        });

        document.body.appendChild(panel);
        updateReplaceList();
    }

    function styleButton(btn, bgColor) {
        btn.style.padding = '5px 10px';
        btn.style.marginRight = '10px';
        btn.style.backgroundColor = bgColor;
        btn.style.color = '#fff';
        btn.style.border = 'none';
        btn.style.cursor = 'pointer';
        btn.style.borderRadius = '4px';
    }

    function styleInput(inp) {
        inp.style.flex = '1';
        inp.style.padding = '4px';
        inp.style.marginRight = '5px';
    }

    function formatTime(d) {
        const y = d.getFullYear();
        const M = String(d.getMonth() + 1).padStart(2, '0');
        const _d = String(d.getDate()).padStart(2, '0');
        const h = String(d.getHours()).padStart(2, '0');
        const m = String(d.getMinutes()).padStart(2, '0');
        const s = String(d.getSeconds()).padStart(2, '0');
        return `${y}${M}${_d}_${h}${m}${s}_`;
    }

    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const element = document.querySelector(selector);
            if (element) {
                resolve(element);
                return;
            }

            const observer = new MutationObserver((mutations, obs) => {
                const element = document.querySelector(selector);
                if (element) {
                    resolve(element);
                    obs.disconnect();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
                reject(`油猴:等待元素 ${selector} 超时`);
            }, timeout);
        });
    }

    function waitForReplaceCompletion(successSelector, failSelector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const checkCompletion = () => {
                const successMessage = document.querySelector(successSelector);
                const failMessage = document.querySelector(failSelector);

                if (successMessage) {
                    console.log('油猴:替换操作成功');
                    resolve('success');
                } else if (failMessage) {
                    console.log('油猴:替换操作失败');
                    resolve('fail');
                } else if (Date.now() - startTime > timeout) {
                    console.log('油猴:替换操作等待超时');
                    reject('timeout');
                } else {
                    requestAnimationFrame(checkCompletion);
                }
            };
            checkCompletion();
        });
    }

    function simulateCtrlH() {
        const event = new KeyboardEvent('keydown', {
            bubbles: true,
            cancelable: true,
            key: 'h',
            code: 'KeyH',
            keyCode: 72,
            charCode: 72,
            ctrlKey: true
        });
        document.dispatchEvent(event);
    }

    async function performReplace(findText, replaceText) {
        try {
            simulateCtrlH();

            if (excelDoc) {
                const excelFindSelector = '.row:not(.replace) .kd-input-inner';
                const excelReplaceSelector = '.row.replace .kd-input-inner';
                const findInput = await waitForElement(excelFindSelector);
                if (findInput) {
                    setInputValue(findInput, findText);
                    console.log(`油猴:查找框已设置为: ${findText}`);
                }
                const replaceInput = await waitForElement(excelReplaceSelector);
                if (replaceInput) {
                    setInputValue(replaceInput, replaceText);
                    console.log(`油猴:替换框已设置为: ${replaceText}`);
                }
                document.querySelectorAll('.kd-radio-input')[1]?.click();
                const replaceAllButton = document.querySelector('.et-cmd-bar-find-panel .wo-button.narrow.replace-all .kd-button.kd-button-secondary.kd-button-lg');
                if (replaceAllButton) {
                    replaceAllButton.click();
                }

                await new Promise(resolve => setTimeout(resolve, 200));

                await waitForReplaceCompletion('.kd-button.kd-button-primary.kd-button-lg.confirm-btn', '.cell_shower .error')
                    .then(async (result) => {
                        if (result === 'success') {
                            document.querySelector('.kd-button.kd-button-primary.kd-button-lg.confirm-btn')?.click();
                            console.log('油猴:替换操作成功');
                        } else if (result === 'fail') {
                            console.log('油猴:无需替换');
                        }
                        await new Promise(resolve => setTimeout(resolve, 200));
                    });
            } else {
                const wordFindSelector = '.component-find-input';
                const wordReplaceSelector = '.component-replace-input';
                const findInput = await waitForElement(wordFindSelector);
                if (findInput) {
                    setInputValue(findInput, findText);
                    console.log(`油猴:查找框已设置为: ${findText}`);
                }
                const replaceInput = await waitForElement(wordReplaceSelector);
                if (replaceInput) {
                    setInputValue(replaceInput, replaceText);
                    console.log(`油猴:替换框已设置为: ${replaceText}`);
                }
                const replaceButton = await waitForElement('.kd-button.kd-button-primary.kd-button-lg');
                if (replaceButton && !replaceButton.disabled) {
                    replaceButton.click();
                    console.log('油猴:已点击全部替换按钮');
                } else {
                    console.log('油猴:全部替换按钮不可点击或未找到');
                    return;
                }

                await new Promise(resolve => setTimeout(resolve, 200));

                await waitForReplaceCompletion('.kd-message-text', '.find-result.empty-result');
                await new Promise(resolve => setTimeout(resolve, 200));
            }
        } catch (error) {
            console.log(`油猴:执行替换操作时出现错误: ${error}`);
        }
    }

    async function performMultipleReplaces(replacePairs) {
        for (const pair of replacePairs) {
            if (pair.findText) {
                await performReplace(pair.findText, pair.replaceText);
            }
        }
    }

    createReplacePanel();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址