Nodeloc 自建图床快捷上传

在 nodeloc.com 的评论框旁添加图片上传功能,支持粘贴/拖拽上传,提示在右下角

// ==UserScript==
// @name         Nodeloc 自建图床快捷上传
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在 nodeloc.com 的评论框旁添加图片上传功能,支持粘贴/拖拽上传,提示在右下角
// @author       BreezeZhang
// @match        https://*.nodeloc.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @connect      nodeloc.com
// @connect      *
// @license      GPL-3.0
// ==/UserScript==

(function() {
    'use strict';

    let config = {
        EASYIMAGE_API_URL: GM_getValue('EASYIMAGE_API_URL', ''),
        EASYIMAGE_TOKEN: GM_getValue('EASYIMAGE_TOKEN', ''),
        LANKONG_API_URL: GM_getValue('LANKONG_API_URL', ''),
        LANKONG_EMAIL: GM_getValue('LANKONG_EMAIL', ''),
        LANKONG_PASSWORD: GM_getValue('LANKONG_PASSWORD', ''),
        UPLOAD_TYPE: GM_getValue('UPLOAD_TYPE', 'easyimage')
    };

    if (!config.EASYIMAGE_API_URL || !config.EASYIMAGE_TOKEN) {
        setTimeout(showConfigModal, 0);
        return;
    }

    setTimeout(initScript, 0);

    function extractDomain(url) {
        try {
            return new URL(url).hostname;
        } catch (e) {
            return '';
        }
    }

    function showConfigModal(isUpdate = false) {
        if (document.querySelector('.easyimage-config-modal')) return;

        const modal = document.createElement('div');
        modal.className = 'easyimage-config-modal';
        modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);display:flex;justify-content:center;align-items:center;z-index:10000;';

        const modalContent = document.createElement('div');
        modalContent.style.cssText = 'background-color:#fff;padding:20px;border-radius:8px;width:350px;box-shadow:0 4px 6px rgba(0,0,0,0.1);display:flex;flex-direction:column;';

        const title = document.createElement('h3');
        title.textContent = isUpdate ? '更新配置' : '初始配置';
        title.style.cssText = 'margin-bottom:20px;text-align:center;color:#333;font-size:1.2em;';
        modalContent.appendChild(title);

        const uploadTypeLabel = document.createElement('label');
        uploadTypeLabel.textContent = '选择图床类型:';
        uploadTypeLabel.style.cssText = 'display:block;margin-bottom:5px;color:#666;font-size:0.9em;';
        modalContent.appendChild(uploadTypeLabel);

        const uploadTypeSelect = document.createElement('select');
        uploadTypeSelect.style.cssText = 'width:100%;padding:8px;margin-bottom:15px;border:1px solid #ddd;border-radius:4px;font-size:1em;';
        ['easyimage', 'lankong'].forEach(type => {
            const option = document.createElement('option');
            option.value = type;
            option.textContent = type === 'easyimage' ? '简单图床' : '兰空图床';
            uploadTypeSelect.appendChild(option);
        });
        uploadTypeSelect.value = config.UPLOAD_TYPE;
        modalContent.appendChild(uploadTypeSelect);

        const apiLabel = document.createElement('label');
        apiLabel.textContent = 'API URL:';
        apiLabel.style.cssText = 'display:block;margin-bottom:5px;color:#666;font-size:0.9em;';
        modalContent.appendChild(apiLabel);

        const apiInput = document.createElement('input');
        apiInput.type = 'text';
        apiInput.value = config.UPLOAD_TYPE === 'easyimage' ? config.EASYIMAGE_API_URL : config.LANKONG_API_URL;
        apiInput.style.cssText = 'width:100%;padding:8px;margin-bottom:15px;border:1px solid #ddd;border-radius:4px;font-size:1em;';
        modalContent.appendChild(apiInput);

        const tokenLabel = document.createElement('label');
        tokenLabel.textContent = 'Token:';
        tokenLabel.style.cssText = apiLabel.style.cssText;
        modalContent.appendChild(tokenLabel);

        const tokenInput = document.createElement('input');
        tokenInput.type = 'text';
        tokenInput.value = config.UPLOAD_TYPE === 'easyimage' ? config.EASYIMAGE_TOKEN : '';
        tokenInput.style.cssText = apiInput.style.cssText;
        modalContent.appendChild(tokenInput);

        const emailLabel = document.createElement('label');
        emailLabel.textContent = '邮箱:';
        emailLabel.style.cssText = apiLabel.style.cssText;
        modalContent.appendChild(emailLabel);

        const emailInput = document.createElement('input');
        emailInput.type = 'text';
        emailInput.value = config.UPLOAD_TYPE === 'lankong' ? config.LANKONG_EMAIL : '';
        emailInput.style.cssText = apiInput.style.cssText;
        modalContent.appendChild(emailInput);

        const passwordLabel = document.createElement('label');
        passwordLabel.textContent = '密码:';
        passwordLabel.style.cssText = apiLabel.style.cssText;
        modalContent.appendChild(passwordLabel);

        const passwordInput = document.createElement('input');
        passwordInput.type = 'password';
        passwordInput.value = config.UPLOAD_TYPE === 'lankong' ? config.LANKONG_PASSWORD : '';
        passwordInput.style.cssText = apiInput.style.cssText;
        modalContent.appendChild(passwordInput);

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'display:flex;justify-content:flex-end;margin-top:20px;';

        const confirmBtn = document.createElement('button');
        confirmBtn.textContent = '保存';
        confirmBtn.style.cssText = 'padding:10px 20px;background-color:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer;font-size:1em;margin-right:10px;transition:background-color 0.3s ease;';
        confirmBtn.addEventListener('click', () => {
            const apiUrl = apiInput.value.trim();
            const token = tokenInput.value.trim();
            const email = emailInput.value.trim();
            const password = passwordInput.value.trim();
            const uploadType = uploadTypeSelect.value;
            if (!apiUrl || (uploadType === 'easyimage' && !token) || (uploadType === 'lankong' && (!email || !password))) {
                alert('请填写所有必填字段!');
                return;
            }
            config.UPLOAD_TYPE = uploadType;
            GM_setValue('UPLOAD_TYPE', config.UPLOAD_TYPE);
            if (uploadType === 'easyimage') {
                config.EASYIMAGE_API_URL = apiUrl;
                config.EASYIMAGE_TOKEN = token;
                GM_setValue('EASYIMAGE_API_URL', config.EASYIMAGE_API_URL);
                GM_setValue('EASYIMAGE_TOKEN', config.EASYIMAGE_TOKEN);
            } else if (uploadType === 'lankong') {
                config.LANKONG_API_URL = apiUrl;
                config.LANKONG_EMAIL = email;
                config.LANKONG_PASSWORD = password;
                GM_setValue('LANKONG_API_URL', config.LANKONG_API_URL);
                GM_setValue('LANKONG_EMAIL', config.LANKONG_EMAIL);
                GM_setValue('LANKONG_PASSWORD', config.LANKONG_PASSWORD);
            }
            document.body.removeChild(modal);
            if (isUpdate) alert('配置更新成功!');
            initScript();
        });
        buttonContainer.appendChild(confirmBtn);

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = '取消';
        cancelBtn.style.cssText = 'padding:10px 20px;background-color:#f44336;color:white;border:none;border-radius:4px;cursor:pointer;font-size:1em;transition:background-color 0.3s ease;';
        cancelBtn.addEventListener('click', () => {
            document.body.removeChild(modal);
            if (!isUpdate) alert('配置未完成,脚本将无法运行!');
        });
        buttonContainer.appendChild(cancelBtn);

        modalContent.appendChild(buttonContainer);

        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        modal.addEventListener('click', e => {
            if (e.target === modal) document.body.removeChild(modal);
        });

        confirmBtn.addEventListener('mouseover', () => confirmBtn.style.backgroundColor = '#45a049');
        confirmBtn.addEventListener('mouseout', () => confirmBtn.style.backgroundColor = '#4CAF50');
        cancelBtn.addEventListener('mouseover', () => cancelBtn.style.backgroundColor = '#da190b');
        cancelBtn.addEventListener('mouseout', () => cancelBtn.style.backgroundColor = '#f44336');

        uploadTypeSelect.addEventListener('change', () => {
            const uploadType = uploadTypeSelect.value;
            if (uploadType === 'easyimage') {
                apiLabel.textContent = 'API URL:';
                tokenLabel.textContent = 'Token:';
                [emailLabel, emailInput, passwordLabel, passwordInput].forEach(el => el.style.display = 'none');
                apiInput.value = config.EASYIMAGE_API_URL || 'https://example.com/api/index.php';
                tokenInput.value = config.EASYIMAGE_TOKEN || '';
            } else if (uploadType === 'lankong') {
                apiLabel.textContent = '兰空图床域名:';
                tokenLabel.textContent = 'Token:';
                [emailLabel, emailInput, passwordLabel, passwordInput].forEach(el => el.style.display = 'block');
                apiInput.value = config.LANKONG_API_URL || 'https://你的兰空图床域名';
                tokenInput.value = '';
                emailInput.value = config.LANKONG_EMAIL || 'YOUR_EMAIL_HERE';
                passwordInput.value = config.LANKONG_PASSWORD || 'YOUR_PASSWORD_HERE';
            }
        });
        uploadTypeSelect.dispatchEvent(new Event('change'));
    }

    function initScript() {
        const checkComposerFooter = () => {
            const composerFooter = document.querySelector('.Composer-footer');
            if (composerFooter && !composerFooter.querySelector('.easyimage-upload-container')) {
                createUploadButton();
                setupPasteAndDrop();
                addPlaceholder();
            } else {
                setTimeout(checkComposerFooter, 100);
            }
        };

        checkComposerFooter();

        const observer = new MutationObserver(checkComposerFooter);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function createUploadButton() {
        const composerFooter = document.querySelector('.Composer-footer');
        if (!composerFooter || composerFooter.querySelector('.easyimage-upload-container')) return false;

        const uploadContainer = document.createElement('li');
        uploadContainer.className = 'item-easyimage easyimage-upload-container';
        uploadContainer.style.cssText = 'display:inline-block';

        const uploadButton = document.createElement('button');
        uploadButton.className = 'Button Button--icon Button--link hasIcon';
        uploadButton.setAttribute('type', 'button');
        uploadButton.setAttribute('title', '');
        uploadButton.setAttribute('aria-label', '上传图片');
        uploadButton.setAttribute('data-original-title', '上传图片');

        const uploadIcon = document.createElement('i');
        uploadIcon.className = 'icon fas fa-image Button-icon';
        uploadIcon.setAttribute('aria-hidden', 'true');

        const uploadLabel = document.createElement('span');
        uploadLabel.className = 'Button-label';
        uploadLabel.textContent = '上传图片';

        uploadButton.appendChild(uploadIcon);
        uploadButton.appendChild(uploadLabel);

        const updateButton = document.createElement('button');
        updateButton.className = 'Button Button--icon Button--link hasIcon';
        updateButton.setAttribute('type', 'button');
        updateButton.setAttribute('title', '');
        updateButton.setAttribute('aria-label', '更新配置');
        updateButton.setAttribute('data-original-title', '更新配置');

        const updateIcon = document.createElement('i');
        updateIcon.className = 'icon fas fa-cog Button-icon';
        updateIcon.setAttribute('aria-hidden', 'true');

        const updateLabel = document.createElement('span');
        updateLabel.className = 'Button-label';
        updateLabel.textContent = '更新配置';

        updateButton.appendChild(updateIcon);
        updateButton.appendChild(updateLabel);

        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = 'image/*';
        fileInput.style.display = 'none';

        uploadContainer.appendChild(uploadButton);
        uploadContainer.appendChild(updateButton);
        uploadContainer.appendChild(fileInput);

        composerFooter.insertBefore(uploadContainer, composerFooter.lastChild);

        uploadButton.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', () => {
            if (fileInput.files.length === 0) return;
            const file = fileInput.files[0];
            uploadButton.textContent = '正在上传...';
            uploadButton.disabled = true;
            uploadImage(file, (success, result) => {
                uploadButton.textContent = '上传图片';
                uploadButton.disabled = false;
                if (success) insertMarkdown(result);
                else alert('上传失败:' + result);
            });
        });
        updateButton.addEventListener('click', () => showConfigModal(true));

        return true;
    }

    function setupPasteAndDrop() {
        const textEditor = document.querySelector('.TextEditor-editor');
        if (!textEditor || textEditor.dataset.pasteDropSetup) return;

        textEditor.dataset.pasteDropSetup = 'true';

        textEditor.addEventListener('paste', event => {
            const items = (event.clipboardData || window.clipboardData).items;
            for (let i = 0; i < items.length; i++) {
                if (items[i].type.indexOf('image') === 0) {
                    event.preventDefault();
                    uploadImage(items[i].getAsFile(), (success, result) => {
                        if (success) insertMarkdown(result);
                        else alert('上传失败:' + result);
                    });
                    break;
                }
            }
        });

        let uploadInProgress = false;
        textEditor.addEventListener('dragover', event => {
            event.preventDefault();
            event.dataTransfer.dropEffect = 'copy';
        });

        textEditor.addEventListener('drop', event => {
            event.preventDefault();
            event.stopPropagation();
            if (uploadInProgress) return;
            const files = event.dataTransfer.files;
            if (files.length > 0 && files[0].type.indexOf('image') === 0) {
                uploadInProgress = true;
                uploadImage(files[0], (success, result) => {
                    uploadInProgress = false;
                    if (success) insertMarkdown(result);
                    else alert('上传失败:' + result);
                });
            }
        });
    }

    function addPlaceholder() {
        const textEditor = document.querySelector('.TextEditor-editor');
        if (!textEditor || textEditor.querySelector('.easyimage-placeholder')) return;

        const placeholder = document.createElement('div');
        placeholder.className = 'easyimage-placeholder';
        placeholder.textContent = '拖拽或粘贴图片可以上传图片';
        placeholder.style.cssText = 'position:absolute;bottom:10px;right:10px;color:#aaa;font-size:12px;pointer-events:none;z-index:1';

        textEditor.parentElement.appendChild(placeholder);

        textEditor.addEventListener('input', () => {
            placeholder.style.display = textEditor.value.trim() ? 'none' : 'block';
        });
    }

    function uploadImage(file, callback) {
        const fileKey = `${file.name}-${file.size}-${file.lastModified}`;
        if (window.uploadedFiles && window.uploadedFiles[fileKey]) {
            return callback(true, window.uploadedFiles[fileKey]);
        }

        if (config.UPLOAD_TYPE === 'easyimage') {
            const formData = new FormData();
            formData.append('image', file);
            formData.append('token', config.EASYIMAGE_TOKEN);

            fetchWithTimeout(config.EASYIMAGE_API_URL, {
                method: 'POST',
                body: formData
            }, 10000)
            .then(response => response.json())
            .then(result => {
                if (result.result === 'success' && result.code === 200) {
                    window.uploadedFiles = window.uploadedFiles || {};
                    window.uploadedFiles[fileKey] = result.url;
                    callback(true, result.url);
                } else {
                    callback(false, result.message || '服务器返回错误');
                }
            })
            .catch(error => callback(false, '网络错误:' + error.message));
        } else if (config.UPLOAD_TYPE === 'lankong') {
            fetchWithTimeout(`${config.LANKONG_API_URL}/api/v1/tokens`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    email: config.LANKONG_EMAIL,
                    password: config.LANKONG_PASSWORD
                })
            }, 10000)
            .then(response => response.json())
            .then(data => {
                if (data.status) {
                    const token = data.data.token;
                    const formData = new FormData();
                    formData.append('image', file);
                    fetchWithTimeout(`${config.LANKONG_API_URL}/api/v1/upload`, {
                        method: 'POST',
                        headers: { 'Authorization': `Bearer ${token}` },
                        body: formData
                    }, 10000)
                    .then(response => response.json())
                    .then(result => {
                        if (result.status) {
                            window.uploadedFiles = window.uploadedFiles || {};
                            window.uploadedFiles[fileKey] = result.data.url;
                            callback(true, result.data.url);
                        } else {
                            callback(false, result.message || '上传失败');
                        }
                    })
                    .catch(error => callback(false, '网络错误:' + error.message));
                } else {
                    callback(false, data.message || '获取Token失败');
                }
            })
            .catch(error => callback(false, '网络错误:' + error.message));
        }
    }

    function insertMarkdown(url) {
        const textEditor = document.querySelector('.TextEditor-editor');
        if (textEditor) {
            const markdown = `![image](${url})`;
            const start = textEditor.selectionStart;
            const end = textEditor.selectionEnd;
            textEditor.value = textEditor.value.substring(0, start) + markdown + textEditor.value.substring(end);
            textEditor.focus();
            textEditor.setSelectionRange(start + markdown.length, start + markdown.length);
            showSuccessMessage();
        }
    }

    function showSuccessMessage() {
        const composerContent = document.querySelector('.Composer-content');
        if (!composerContent) return;

        const successMsg = document.createElement('div');
        successMsg.textContent = '🎉 图片上传成功!';
        successMsg.style.cssText = 'color:#4CAF50;margin:5px 0';
        composerContent.insertBefore(successMsg, composerContent.firstChild);
        setTimeout(() => {
            if (successMsg.parentNode) {
                successMsg.parentNode.removeChild(successMsg);
            }
        }, 2000);
    }

    function fetchWithTimeout(resource, options = {}, timeout = 10000) {
        return Promise.race([
            fetch(resource, options),
            new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时')), timeout))
        ]);
    }
})();

QingJ © 2025

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