您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 V2EX 评论区快速上传图片并插入链接
// ==UserScript== // @name V2EX Image Uploader // @version 1.1 // @description 在 V2EX 评论区快速上传图片并插入链接 // @author Dogxi // @match https://www.v2ex.com/t/* // @match https://v2ex.com/t/* // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api.imgur.com // @license MIT // @namespace http://tampermonkey.net/1436051 // ==/UserScript== (function () { "use strict"; const IMGUR_CLIENT_ID_KEY = "imgurClientId"; let CLIENT_ID = GM_getValue(IMGUR_CLIENT_ID_KEY, null); const STYLE = ` .imgur-upload-btn { background: none; border: none; color: #778087; cursor: pointer; font-size: 13px; padding: 0; margin-left: 15px; text-decoration: none; transition: color 0.2s ease; } .imgur-upload-btn:hover { color: #4d5256; text-decoration: underline; } .hidden { display: none !important; } .imgur-upload-modal { 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: 9999; outline: none; } .imgur-upload-modal-content { background-color: #fff; padding: 20px; border-radius: 3px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); max-width: 450px; width: 90%; position: relative; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .imgur-upload-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #e2e2e2; } .imgur-upload-modal-title { font-size: 15px; font-weight: normal; color: #000; } .imgur-upload-modal-close { cursor: pointer; font-size: 18px; color: #ccc; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; transition: color 0.2s ease; } .imgur-upload-modal-close:hover { color: #999; } .imgur-upload-dropzone { border: 1px dashed #ccc; padding: 25px; text-align: center; margin-bottom: 15px; cursor: pointer; border-radius: 3px; transition: border-color 0.2s ease; font-size: 13px; color: #666; outline: none; } .imgur-upload-dropzone:hover { border-color: #999; } .imgur-upload-dropzone:focus { border-color: #778087; background-color: #f9f9f9; } .imgur-upload-dropzone.dragover { border-color: #778087; background-color: #f9f9f9; } .imgur-upload-preview { margin-top: 10px; max-width: 100%; max-height: 150px; border-radius: 2px; } .imgur-upload-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 10px; border-top: 1px solid #e2e2e2; } .imgur-upload-config-btn { background: none; border: none; color: #778087; cursor: pointer; font-size: 12px; padding: 0; } .imgur-upload-config-btn:hover { color: #4d5256; text-decoration: underline; } .imgur-upload-submit-btn { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 3px; color: #333; cursor: pointer; font-size: 12px; padding: 6px 12px; transition: all 0.2s ease; } .imgur-upload-submit-btn:hover { background-color: #e8e8e8; } .imgur-upload-submit-btn:disabled { background-color: #f9f9f9; color: #ccc; cursor: not-allowed; } .imgur-upload-config-panel { margin-top: 10px; padding: 10px; background-color: #f9f9f9; border-radius: 3px; border: 1px solid #e2e2e2; } .imgur-upload-config-row { display: flex; align-items: center; margin-bottom: 8px; } .imgur-upload-config-row:last-child { margin-bottom: 0; } .imgur-upload-config-label { font-size: 12px; color: #666; width: 70px; flex-shrink: 0; } .imgur-upload-config-input { flex: 1; padding: 3px 6px; border: 1px solid #ccc; border-radius: 2px; font-size: 12px; } .imgur-upload-config-save { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 2px; color: #333; cursor: pointer; font-size: 11px; margin-left: 6px; padding: 3px 8px; } .imgur-upload-config-save:hover { background-color: #e8e8e8; } .imgur-upload-modal-status { color: #666; font-size: 12px; text-align: center; } .imgur-upload-modal-status.success { color: #5cb85c; } .imgur-upload-modal-status.error { color: #d9534f; } `; // 添加样式到页面 function addStyle() { const styleElement = document.createElement("style"); styleElement.textContent = STYLE; document.head.appendChild(styleElement); } // 创建上传弹窗 function createUploadModal(textareaElement) { const modal = document.createElement("div"); modal.className = "imgur-upload-modal"; const content = document.createElement("div"); content.className = "imgur-upload-modal-content"; content.innerHTML = ` <div class="imgur-upload-modal-header"> <div class="imgur-upload-modal-title">上传图片</div> <div class="imgur-upload-modal-close">×</div> </div> <div class="imgur-upload-dropzone"> <div>点击选择图片、拖拽图片到此处,或直接粘贴图片</div> <div style="font-size: 11px; color: #999; margin-top: 5px;">支持 JPG, PNG, GIF 格式</div> </div> <div class="imgur-upload-actions"> <button class="imgur-upload-config-btn">⚙️ 配置</button> <button class="imgur-upload-submit-btn" disabled>确认上传</button> </div> <div class="imgur-upload-config-panel hidden"> <div class="imgur-upload-config-row"> <div class="imgur-upload-config-label">Imgur ID:</div> <input type="text" class="imgur-upload-config-input" placeholder="请输入 Imgur Client ID" value="${ CLIENT_ID || "" }"> <button class="imgur-upload-config-save">保存</button> </div> <div style="font-size: 11px; color: #666; margin-top: 8px;"> 在 <a href="https://api.imgur.com/oauth2/addclient" target="_blank">https://api.imgur.com/oauth2/addclient</a> 注册获取(无回调) </div> </div> `; modal.appendChild(content); document.body.appendChild(modal); setupModalEvents(modal, textareaElement); return modal; } // 设置弹窗事件监听 function setupModalEvents(modal, textareaElement) { const closeBtn = modal.querySelector(".imgur-upload-modal-close"); const dropzone = modal.querySelector(".imgur-upload-dropzone"); const configBtn = modal.querySelector(".imgur-upload-config-btn"); const configPanel = modal.querySelector(".imgur-upload-config-panel"); const configInput = modal.querySelector(".imgur-upload-config-input"); const configSave = modal.querySelector(".imgur-upload-config-save"); const submitBtn = modal.querySelector(".imgur-upload-submit-btn"); let selectedFile = null; function closeModal() { document.body.removeChild(modal); } closeBtn.addEventListener("click", closeModal); modal.addEventListener("click", function (e) { if (e.target === modal) closeModal(); }); configBtn.addEventListener("click", function () { configPanel.classList.toggle("hidden"); }); configSave.addEventListener("click", function () { const newClientId = configInput.value.trim(); if (newClientId) { GM_setValue(IMGUR_CLIENT_ID_KEY, newClientId); CLIENT_ID = newClientId; configPanel.classList.add("hidden"); showStatusInModal(modal, "配置已保存", "success"); } }); const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none"; modal.appendChild(fileInput); dropzone.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", function (e) { handleFileSelect(e.target.files[0]); }); dropzone.addEventListener("dragover", function (e) { e.preventDefault(); dropzone.classList.add("dragover"); }); dropzone.addEventListener("dragleave", function (e) { e.preventDefault(); dropzone.classList.remove("dragover"); }); dropzone.addEventListener("drop", function (e) { e.preventDefault(); dropzone.classList.remove("dragover"); const files = e.dataTransfer.files; if (files.length > 0) { handleFileSelect(files[0]); } }); // 在整个弹窗上监听粘贴事件 modal.addEventListener("paste", function (e) { e.preventDefault(); const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.indexOf("image") !== -1) { const file = item.getAsFile(); if (file) { handleFileSelect(file); break; } } } }); // 让弹窗能够接收键盘事件 modal.setAttribute("tabindex", "0"); modal.focus(); // 处理文件选择 function handleFileSelect(file) { if (!file || !file.type.match(/image\/.*/)) { showStatusInModal(modal, "请选择图片文件", "error"); return; } selectedFile = file; const reader = new FileReader(); reader.onload = function (e) { const preview = modal.querySelector(".imgur-upload-preview"); if (preview) preview.remove(); const img = document.createElement("img"); img.src = e.target.result; img.className = "imgur-upload-preview"; dropzone.appendChild(img); submitBtn.disabled = false; dropzone.querySelector("div").textContent = "已选择: " + file.name; }; reader.readAsDataURL(file); } submitBtn.addEventListener("click", function () { if (!selectedFile) return; if (!CLIENT_ID) { showStatusInModal(modal, "请先配置 Imgur Client ID", "error"); configPanel.classList.remove("hidden"); return; } submitBtn.disabled = true; submitBtn.textContent = "上传中..."; uploadToImgur(selectedFile, textareaElement, modal); }); } // 在弹窗中显示状态信息 function showStatusInModal(modal, message, type) { let statusEl = modal.querySelector(".imgur-upload-modal-status"); if (!statusEl) { statusEl = document.createElement("div"); statusEl.className = "imgur-upload-modal-status"; statusEl.style.cssText = "margin-top: 10px; font-size: 12px; text-align: center;"; modal.querySelector(".imgur-upload-modal-content").appendChild(statusEl); } statusEl.textContent = message; statusEl.className = "imgur-upload-modal-status " + (type || ""); if (type === "success") { setTimeout(() => (statusEl.textContent = ""), 3000); } } // 上传图片到 Imgur function uploadToImgur(file, textareaElement, modal) { if (!file.type.match(/image\/.*/)) { showStatusInModal(modal, "请选择图片文件", "error"); const submitBtn = modal.querySelector(".imgur-upload-submit-btn"); submitBtn.disabled = false; submitBtn.textContent = "确认上传"; return; } const formData = new FormData(); formData.append("image", file); GM_xmlhttpRequest({ method: "POST", url: "https://api.imgur.com/3/image", headers: { Authorization: "Client-ID " + CLIENT_ID, }, data: formData, responseType: "json", onload: function (response) { const submitBtn = modal.querySelector(".imgur-upload-submit-btn"); try { let responseData; if (typeof response.response === "string") { responseData = JSON.parse(response.response); } else { responseData = response.response; } if (response.status === 200 && responseData && responseData.success) { const imageUrl = responseData.data.link; insertLinkIntoTextarea(textareaElement, imageUrl, file.name); showStatusInModal(modal, "上传成功!", "success"); setTimeout(() => { document.body.removeChild(modal); }, 1500); } else { let errorMessage = ""; if (response.status === 400) { if ( responseData && responseData.data && responseData.data.error ) { if ( responseData.data.error === "These actions are forbidden." ) { errorMessage = "Client ID 无效或已被禁用,请检查配置"; } else { errorMessage = responseData.data.error; } } else { errorMessage = "Client ID 配置错误"; } } else if (response.status === 403) { errorMessage = "访问被拒绝,请检查 Client ID 权限"; } else if (response.status === 429) { errorMessage = "请求过于频繁,请稍后再试"; } else { errorMessage = `上传失败 (${response.status})`; } console.error("Imgur 上传错误:", response); showStatusInModal(modal, errorMessage, "error"); if (response.status === 400 || response.status === 403) { const configPanel = modal.querySelector( ".imgur-upload-config-panel" ); configPanel.classList.remove("hidden"); } submitBtn.disabled = false; submitBtn.textContent = "确认上传"; } } catch (e) { console.error("解析响应失败:", e, response); showStatusInModal(modal, "响应解析失败,请重试", "error"); submitBtn.disabled = false; submitBtn.textContent = "确认上传"; } }, onerror: function (error) { console.error("GM_xmlhttpRequest 错误:", error); showStatusInModal(modal, "网络请求失败,请检查连接", "error"); const submitBtn = modal.querySelector(".imgur-upload-submit-btn"); submitBtn.disabled = false; submitBtn.textContent = "确认上传"; }, ontimeout: function () { console.error("Imgur 上传超时"); showStatusInModal(modal, "上传超时,请重试", "error"); const submitBtn = modal.querySelector(".imgur-upload-submit-btn"); submitBtn.disabled = false; submitBtn.textContent = "确认上传"; }, }); } // 将图片链接插入到文本框 function insertLinkIntoTextarea(textareaElement, imageUrl, fileName) { const altText = fileName ? fileName.split(".")[0] : "image"; const textToInsert = imageUrl; const currentValue = textareaElement.value; const selectionStart = textareaElement.selectionStart; const selectionEnd = textareaElement.selectionEnd; const newText = currentValue.substring(0, selectionStart) + textToInsert + currentValue.substring(selectionEnd); textareaElement.value = newText; const newCursorPosition = selectionStart + textToInsert.length; textareaElement.selectionStart = newCursorPosition; textareaElement.selectionEnd = newCursorPosition; textareaElement.focus(); textareaElement.dispatchEvent( new Event("input", { bubbles: true, cancelable: true }) ); } // 在页面头部添加上传按钮 function addUploadButtonToHeader() { const replyBox = document.getElementById("reply-box"); if (!replyBox) return; const headerCell = replyBox.querySelector(".cell.flex-one-row"); if (!headerCell) return; if (headerCell.querySelector(".imgur-upload-btn")) return; const leftDiv = headerCell.querySelector("div:first-child"); if (leftDiv) { const uploadBtn = document.createElement("a"); uploadBtn.className = "imgur-upload-btn"; uploadBtn.textContent = "上传"; uploadBtn.href = "javascript:void(0);"; uploadBtn.title = "上传图片"; uploadBtn.style.marginLeft = "10px"; leftDiv.appendChild(uploadBtn); uploadBtn.addEventListener("click", function (e) { e.preventDefault(); const textarea = document.getElementById("reply_content"); if (textarea) { createUploadModal(textarea); } }); } } // 查找并添加上传按钮 function findTextareasAndAddButtons() { addUploadButtonToHeader(); setupMutationObserver(); } // 监听DOM变化 function setupMutationObserver() { const observer = new MutationObserver(function (mutations) { let shouldCheck = false; mutations.forEach(function (mutation) { mutation.addedNodes.forEach(function (node) { if ( node.nodeType === Node.ELEMENT_NODE && (node.id === "reply-box" || node.querySelector("#reply-box")) ) { shouldCheck = true; } }); }); if (shouldCheck) { setTimeout(addUploadButtonToHeader, 100); } }); observer.observe(document.body, { childList: true, subtree: true, }); } // 初始化脚本 function init() { addStyle(); setTimeout(findTextareasAndAddButtons, 100); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();