// ==UserScript== // @name NodeSeek 图片上传工具 // @namespace https://www.nodeseek.com/ // @version 1.5.0 // @description 为 NodeSeek 论坛编辑器添加一键图片上传功能,支持多图片同时上传,自动压缩大图片,支持自定义 Token // @author Claude 3.7 // @match https://www.nodeseek.com/* // @grant none // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/534765/NodeSeek%20%E5%9B%BE%E7%89%87%E4%B8%8A%E4%BC%A0%E5%B7%A5%E5%85%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/534765/NodeSeek%20%E5%9B%BE%E7%89%87%E4%B8%8A%E4%BC%A0%E5%B7%A5%E5%85%B7.meta.js // ==/UserScript== (function () { "use strict"; /** * 应用配置 */ const CONFIG = { // 图床API配置 UPLOAD: { URL: "https://i.111666.best/image", BASE_URL: "https://i.111666.best", MAX_SIZE: 6.7, // MB TARGET_SIZE: 6.3, // 压缩目标大小 MB }, // DOM选择器 SELECTORS: { TOOLBAR: ".mde-toolbar", PIC_BUTTON: ".i-icon-pic", CODEMIRROR: "#code-mirror-editor .CodeMirror", TEXTAREA: ".CodeMirror textarea", }, // UI配置 UI: { BUTTON_CLASS: "ns-image-uploader", SETTINGS_BUTTON_CLASS: "ns-settings-button", NOTIFICATION_DURATION: 3000, MAX_CONCURRENT_UPLOADS: 3, // 最大并发上传数 }, // 存储键 STORAGE_KEYS: { PRIMARY_TOKEN: "ns_uploader_primary_token", BACKUP_TOKEN: "ns_uploader_backup_token", }, }; /** * 图片上传助手 */ class ImageUploader { constructor() { this.loadSettings(); this.setupUI(); this.addEventListeners(); this.observeDOM(); this.uploadQueue = []; // 上传队列 this.activeUploads = 0; // 当前活跃的上传任务数 this.uploadResults = { success: 0, failed: 0, total: 0 }; // 上传结果统计 this.lastUploadTask = null; // 存储最后一个上传任务的引用 } /** * 从本地存储加载设置 */ loadSettings() { // 初始化 Token 设置 this.settings = { primaryToken: localStorage.getItem(CONFIG.STORAGE_KEYS.PRIMARY_TOKEN) || this.generateRandomToken(), backupToken: localStorage.getItem(CONFIG.STORAGE_KEYS.BACKUP_TOKEN) || this.generateRandomToken(), }; // 如果是首次使用,保存默认生成的随机 Token if (!localStorage.getItem(CONFIG.STORAGE_KEYS.PRIMARY_TOKEN)) { this.saveSettings(); } } /** * 保存设置到本地存储 */ saveSettings() { localStorage.setItem( CONFIG.STORAGE_KEYS.PRIMARY_TOKEN, this.settings.primaryToken, ); localStorage.setItem( CONFIG.STORAGE_KEYS.BACKUP_TOKEN, this.settings.backupToken, ); } /** * 生成随机 Token * @returns {string} 随机生成的 Token */ generateRandomToken() { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const length = 24; let result = ""; const charactersLength = characters.length; for (let i = 0; i < length; i++) { result += characters.charAt( Math.floor(Math.random() * charactersLength), ); } return result; } /** * 设置用户界面 */ setupUI() { // 注入样式 const style = document.createElement("style"); style.textContent = ` /* 上传按钮样式 */ .${CONFIG.UI.BUTTON_CLASS}, .${CONFIG.UI.SETTINGS_BUTTON_CLASS} { cursor: pointer; transition: all 0.2s ease; padding: 2px; display: inline-flex; align-items: center; justify-content: center; } .${CONFIG.UI.BUTTON_CLASS}:hover, .${CONFIG.UI.SETTINGS_BUTTON_CLASS}:hover { background-color: rgba(0, 0, 0, 0.1); border-radius: 4px; } /* 加载动画 */ .ns-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(0, 0, 0, 0.1); border-top: 2px solid #3498db; border-radius: 50%; animation: ns-spin 1s linear infinite; } @keyframes ns-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 通知样式 */ .ns-notification { position: fixed; bottom: 20px; right: 20px; padding: 10px 16px; border-radius: 4px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); color: white; font-size: 14px; z-index: 10000; transition: all 0.3s ease; opacity: 0; transform: translateY(20px); max-width: 400px; } .ns-notification.success { background-color: #4CAF50; } .ns-notification.error { background-color: #f44336; } .ns-notification.info { background-color: #2196F3; } .ns-notification.visible { opacity: 1; transform: translateY(0); } /* 多图上传进度显示 */ .ns-upload-progress { margin-top: 5px; font-size: 12px; } /* 进度条容器 */ .ns-progress-bar-container { height: 6px; background-color: rgba(0, 0, 0, 0.1); border-radius: 3px; margin-top: 4px; overflow: hidden; } /* 进度条 */ .ns-progress-bar { height: 100%; background-color: #4CAF50; border-radius: 3px; transition: width 0.3s ease; } /* 设置面板样式 */ .ns-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10001; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s; } .ns-settings-modal.visible { opacity: 1; visibility: visible; } .ns-settings-container { background-color: white; border-radius: 8px; padding: 20px; width: 90%; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); transform: translateY(20px); transition: transform 0.3s ease; } .ns-settings-modal.visible .ns-settings-container { transform: translateY(0); } .ns-settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .ns-settings-title { font-size: 18px; font-weight: bold; margin: 0; } .ns-settings-close { background: none; border: none; font-size: 20px; cursor: pointer; color: #999; } .ns-settings-close:hover { color: #333; } .ns-settings-form { margin-bottom: 20px; } .ns-input-group { margin-bottom: 16px; } .ns-input-group label { display: block; margin-bottom: 5px; font-weight: 500; } .ns-token-input-wrapper { display: flex; gap: 8px; } .ns-token-input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .ns-token-input:focus { border-color: #3498db; outline: none; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); } .ns-random-btn { padding: 8px 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .ns-random-btn:hover { background-color: #e5e5e5; } .ns-settings-footer { display: flex; justify-content: flex-end; gap: 10px; } .ns-settings-btn { padding: 8px 16px; border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; } .ns-save-btn { background-color: #3498db; color: white; border: none; } .ns-save-btn:hover { background-color: #2980b9; } .ns-cancel-btn { background-color: #f5f5f5; border: 1px solid #ddd; color: #333; } .ns-cancel-btn:hover { background-color: #e5e5e5; } .ns-description { color: #666; font-size: 13px; margin-top: 5px; line-height: 1.4; } .ns-settings-container p { margin-top: 0; margin-bottom: 1em; line-height: 1.5; } .ns-settings-version { color: #999; font-size: 12px; text-align: right; margin-top: 10px; } `; document.head.appendChild(style); // 添加上传按钮和设置按钮 this.addButtons(); // 创建设置面板(一开始是隐藏的) this.createSettingsModal(); } /** * 创建设置对话框 */ createSettingsModal() { // 如果设置面板已存在,不重复创建 if (document.querySelector(".ns-settings-modal")) { return; } // 创建设置面板 const modal = document.createElement("div"); modal.className = "ns-settings-modal"; modal.innerHTML = `

图片上传设置

自定义上传 Token,保护您的上传权限不被他人使用。

主要上传请求会优先使用此 Token
当主要 Token 失效时,将使用此备用 Token
v1.5.0
`; document.body.appendChild(modal); // 绑定事件 // 关闭按钮 modal .querySelector(".ns-settings-close") .addEventListener("click", () => { this.hideSettingsModal(); }); // 取消按钮 modal.querySelector(".ns-cancel-btn").addEventListener("click", () => { this.hideSettingsModal(); }); // 保存按钮 modal.querySelector(".ns-save-btn").addEventListener("click", () => { this.saveTokenSettings(); }); // 随机按钮事件 modal.querySelectorAll(".ns-random-btn").forEach((button) => { button.addEventListener("click", (e) => { const target = e.target.dataset.target; const randomToken = this.generateRandomToken(); if (target === "primary") { modal.querySelector("#ns-primary-token").value = randomToken; } else if (target === "backup") { modal.querySelector("#ns-backup-token").value = randomToken; } }); }); // 点击模态框背景关闭 modal.addEventListener("click", (e) => { if (e.target === modal) { this.hideSettingsModal(); } }); } /** * 显示设置对话框 */ showSettingsModal() { const modal = document.querySelector(".ns-settings-modal"); if (modal) { // 更新输入框值为当前设置 modal.querySelector("#ns-primary-token").value = this.settings.primaryToken; modal.querySelector("#ns-backup-token").value = this.settings.backupToken; modal.classList.add("visible"); // 防止背景滚动 document.body.style.overflow = "hidden"; } } /** * 隐藏设置对话框 */ hideSettingsModal() { const modal = document.querySelector(".ns-settings-modal"); if (modal) { modal.classList.remove("visible"); // 恢复背景滚动 document.body.style.overflow = ""; } } /** * 保存 Token 设置 */ saveTokenSettings() { const primaryTokenInput = document.querySelector("#ns-primary-token"); const backupTokenInput = document.querySelector("#ns-backup-token"); if (!primaryTokenInput.value.trim()) { this.showNotification("主要 Token 不能为空", "error"); return; } if (!backupTokenInput.value.trim()) { this.showNotification("备用 Token 不能为空", "error"); return; } // 更新设置 this.settings.primaryToken = primaryTokenInput.value.trim(); this.settings.backupToken = backupTokenInput.value.trim(); // 保存到本地存储 this.saveSettings(); // 隐藏设置面板 this.hideSettingsModal(); // 显示成功通知 this.showNotification("设置已保存", "success"); } /** * 添加上传按钮和设置按钮到工具栏 */ addButtons() { // 如果按钮已存在,不重复添加 if (document.querySelector(`.${CONFIG.UI.BUTTON_CLASS}`)) { return; } // 查找工具栏和图片按钮 const toolbar = document.querySelector(CONFIG.SELECTORS.TOOLBAR); const picButton = toolbar?.querySelector(CONFIG.SELECTORS.PIC_BUTTON); if (!toolbar || !picButton) return; // 创建上传按钮 const uploadButton = document.createElement("span"); uploadButton.className = `toolbar-item i-icon ${CONFIG.UI.BUTTON_CLASS}`; uploadButton.title = `上传图片 (最大${CONFIG.UPLOAD.MAX_SIZE}MB,超限自动压缩,支持多图同时上传)`; uploadButton.innerHTML = this.getUploadButtonSVG(); // 添加按钮点击事件 uploadButton.addEventListener("click", () => this.handleUploadClick(uploadButton), ); // 创建设置按钮 const settingsButton = document.createElement("span"); settingsButton.className = `toolbar-item i-icon ${CONFIG.UI.SETTINGS_BUTTON_CLASS}`; settingsButton.title = `图片上传设置`; settingsButton.innerHTML = this.getSettingsButtonSVG(); // 添加设置按钮点击事件 settingsButton.addEventListener("click", () => this.showSettingsModal()); // 将按钮添加到工具栏 picButton.parentNode.insertBefore(uploadButton, picButton.nextSibling); picButton.parentNode.insertBefore( settingsButton, uploadButton.nextSibling, ); } /** * 获取上传按钮的SVG内容 */ getUploadButtonSVG() { return ` `; } /** * 获取设置按钮的SVG内容 */ getSettingsButtonSVG() { return ` `; } /** * 添加事件监听器 */ addEventListeners() { // DOM已加载完成 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => this.addButtons()); } else { this.addButtons(); } } /** * 观察DOM变化,处理动态加载的编辑器 */ observeDOM() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes && mutation.addedNodes.length) { const toolbar = document.querySelector(CONFIG.SELECTORS.TOOLBAR); if ( toolbar && !document.querySelector(`.${CONFIG.UI.BUTTON_CLASS}`) ) { this.addButtons(); } } } }); observer.observe(document.body, { childList: true, subtree: true, }); } /** * 处理上传按钮点击 * @param {HTMLElement} button - 上传按钮元素 */ handleUploadClick(button) { // 创建隐藏的文件输入框 const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.multiple = true; // 支持多文件选择 fileInput.style.display = "none"; document.body.appendChild(fileInput); // 监听文件选择 fileInput.addEventListener("change", async () => { try { if (fileInput.files && fileInput.files.length > 0) { const files = Array.from(fileInput.files); const originalButtonContent = button.innerHTML; const originalButtonTitle = button.title; // 设置按钮加载状态 this.setButtonLoading(button, true, "准备上传..."); // 初始化上传状态 this.uploadResults = { success: 0, failed: 0, total: files.length, }; // 创建进度通知 this.showProgressNotification( `准备上传 ${files.length} 张图片...`, 0, ); // 一次性为所有文件创建占位符 const placeholders = {}; files.forEach((file) => { const placeholderId = `upload-${Date.now()}-${Math.floor(Math.random() * 1000)}-${file.name.replace(/[^a-z0-9]/gi, "")}`; const placeholderText = `![正在上传 ${file.name}...](uploading#${placeholderId})`; // 插入占位符到编辑器 this.insertMarkdownImage(file.name, placeholderText); // 存储占位符ID与文件的关联 placeholders[file.name] = placeholderId; }); // 将所有文件添加到上传队列 files.forEach((file) => { const task = { file, button, originalButtonContent, originalButtonTitle, placeholderId: placeholders[file.name], // 存储对应的占位符ID }; this.uploadQueue.push(task); // 保存最后一个任务的引用,确保队列清空后仍能恢复按钮状态 this.lastUploadTask = task; }); // 开始处理上传队列 this.processUploadQueue(); } } catch (error) { console.error("处理错误:", error); this.showNotification(`错误: ${error.message}`, "error"); // 出错时也要恢复按钮状态 this.setButtonLoading( button, false, originalButtonTitle || `上传图片 (最大${CONFIG.UPLOAD.MAX_SIZE}MB,超限自动压缩,支持多图同时上传)`, originalButtonContent, ); } finally { // 清理文件输入框 document.body.removeChild(fileInput); } }); // 触发文件选择 fileInput.click(); } /** * 处理上传队列 */ processUploadQueue() { // 检查是否有正在等待的上传任务 if (this.uploadQueue.length === 0 && this.activeUploads === 0) { // 所有上传已完成,首先移除进度通知 const progressNotification = document.querySelector( ".ns-progress-notification", ); if (progressNotification) { progressNotification.classList.remove("visible"); // 等待进度通知消失后再显示最终结果 progressNotification.addEventListener( "transitionend", () => { if (progressNotification.parentNode) { document.body.removeChild(progressNotification); // 显示最终结果通知 const { success, failed, total } = this.uploadResults; if (failed === 0) { this.showNotification( `所有 ${total} 张图片上传成功!`, "success", ); } else { this.showNotification( `上传完成: ${success} 成功, ${failed} 失败 (共 ${total} 张)`, failed > 0 ? "error" : "success", ); } } }, { once: true }, ); } else { // 如果没有进度通知,直接显示结果 const { success, failed, total } = this.uploadResults; if (failed === 0) { this.showNotification(`所有 ${total} 张图片上传成功!`, "success"); } else { this.showNotification( `上传完成: ${success} 成功, ${failed} 失败 (共 ${total} 张)`, failed > 0 ? "error" : "success", ); } } // 恢复按钮状态 const { button, originalButtonContent, originalButtonTitle } = this.uploadQueue.length > 0 ? this.uploadQueue[0] : this.lastUploadTask; if (button) { this.setButtonLoading( button, false, originalButtonTitle || `上传图片 (最大${CONFIG.UPLOAD.MAX_SIZE}MB,超限自动压缩,支持多图同时上传)`, originalButtonContent, ); } return; } // 控制并发上传数量 while ( this.uploadQueue.length > 0 && this.activeUploads < CONFIG.UI.MAX_CONCURRENT_UPLOADS ) { const uploadTask = this.uploadQueue.shift(); this.activeUploads++; // 异步处理单个文件上传 this.uploadSingleFile(uploadTask).finally(() => { this.activeUploads--; // 更新进度显示 const { success, failed, total } = this.uploadResults; const completed = success + failed; const progress = (completed / total) * 100; this.showProgressNotification( `上传进度: ${completed}/${total} (${success} 成功, ${failed} 失败)`, progress, ); // 继续处理队列中的下一个文件 this.processUploadQueue(); }); } } /** * 上传单个文件 * @param {Object} uploadTask - 上传任务对象 * @returns {Promise} */ async uploadSingleFile(uploadTask) { const { file, button, placeholderId } = uploadTask; try { // 检查是否需要压缩 let fileToUpload = file; const fileSizeInMB = file.size / (1024 * 1024); if (fileSizeInMB > CONFIG.UPLOAD.MAX_SIZE) { // 压缩图片 try { fileToUpload = await this.compressImage(file); // 检查压缩后的大小 const compressedSizeMB = fileToUpload.size / (1024 * 1024); console.log( `图片已压缩: ${fileSizeInMB.toFixed(2)}MB -> ${compressedSizeMB.toFixed(2)}MB`, ); } catch (error) { console.error("压缩失败:", error); this.uploadResults.failed++; this.replaceMarkdownPlaceholder( placeholderId, file.name, "upload-failed", `![压缩失败: ${file.name}](upload-failed)`, ); return; } } // 上传图片 const imageUrl = await this.uploadImage(fileToUpload); // 替换占位的Markdown代码 this.replaceMarkdownPlaceholder(placeholderId, file.name, imageUrl); // 更新成功计数 this.uploadResults.success++; } catch (error) { console.error(`上传失败 (${file.name}):`, error); // 更新失败计数 this.uploadResults.failed++; // 上传失败时更新占位符 this.replaceMarkdownPlaceholder( placeholderId, file.name, "upload-failed", `![上传失败: ${file.name}](upload-failed)`, ); } } /** * 显示进度通知 * @param {string} message - 通知消息 * @param {number} progress - 进度百分比 (0-100) */ showProgressNotification(message, progress) { // 查找已有的进度通知或创建新的 let notification = document.querySelector(".ns-progress-notification"); if (!notification) { notification = document.createElement("div"); notification.className = "ns-notification ns-progress-notification info"; notification.innerHTML = `
${message}
`; document.body.appendChild(notification); // 使通知可见 setTimeout(() => { notification.classList.add("visible"); }, 10); } else { // 更新现有通知 notification.querySelector("div:first-child").textContent = message; notification.querySelector(".ns-progress-bar").style.width = `${progress}%`; } // 如果进度到100%,5秒后自动移除进度通知 if (progress >= 100) { setTimeout(() => { if (notification && notification.parentNode) { notification.classList.remove("visible"); notification.addEventListener( "transitionend", () => { if (notification.parentNode) { document.body.removeChild(notification); } }, { once: true }, ); } }, 5000); } } /** * 设置按钮加载状态 * @param {HTMLElement} button - 按钮元素 * @param {boolean} isLoading - 是否为加载状态 * @param {string} title - 按钮标题 * @param {string} [content] - 按钮内容 (仅在非加载状态有效) */ setButtonLoading(button, isLoading, title, content = null) { if (isLoading) { button.innerHTML = ''; button.title = title; } else { button.innerHTML = content || this.getUploadButtonSVG(); button.title = title; } } /** * 压缩图片 * @param {File} file - 要压缩的图片文件 * @returns {Promise} - 返回压缩后的图片文件 */ async compressImage(file) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { // 创建canvas const canvas = document.createElement("canvas"); let { width, height } = img; // 如果图片尺寸很大,适当缩小以提高压缩效率 const MAX_DIMENSION = 4000; // 最大尺寸限制 if (width > MAX_DIMENSION || height > MAX_DIMENSION) { if (width > height) { height = Math.floor(height * (MAX_DIMENSION / width)); width = MAX_DIMENSION; } else { width = Math.floor(width * (MAX_DIMENSION / height)); height = MAX_DIMENSION; } } // 设置canvas尺寸 canvas.width = width; canvas.height = height; // 绘制图片到canvas const ctx = canvas.getContext("2d"); ctx.fillStyle = "#FFFFFF"; // 设置白色背景 ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); // 目标大小范围 const targetSize = CONFIG.UPLOAD.TARGET_SIZE; // 目标为6.3MB // 逐步尝试不同的压缩质量 const compressWithQuality = (quality) => { try { canvas.toBlob( (blob) => { if (!blob) { reject(new Error("压缩过程中出错")); return; } // 检查压缩后的大小 const sizeInMB = blob.size / (1024 * 1024); // 如果大小已经在目标范围内,或已达到最低质量 if ( (sizeInMB <= CONFIG.UPLOAD.MAX_SIZE && sizeInMB >= targetSize * 0.9) || quality <= 0.5 ) { // 创建新的文件对象 const compressedFile = new File([blob], file.name, { type: "image/jpeg", lastModified: Date.now(), }); resolve(compressedFile); } // 如果大小过大,继续降低质量 else if (sizeInMB > CONFIG.UPLOAD.MAX_SIZE) { // 降低质量,但避免质量过低 const newQuality = Math.max(0.5, quality - 0.05); setTimeout(() => compressWithQuality(newQuality), 0); } // 如果大小太小,尝试提高质量 else if (sizeInMB < targetSize * 0.85 && quality < 0.95) { // 提高质量,但不超过0.95 const newQuality = Math.min(0.95, quality + 0.05); setTimeout(() => compressWithQuality(newQuality), 0); } // 如果大小在合理的范围内,接受当前结果 else { const compressedFile = new File([blob], file.name, { type: "image/jpeg", lastModified: Date.now(), }); resolve(compressedFile); } }, "image/jpeg", quality, ); } catch (err) { reject(new Error(`压缩过程中出错: ${err.message}`)); } }; // 开始压缩,初始质量为0.9 compressWithQuality(0.9); }; img.onerror = () => { reject(new Error("图片加载失败")); }; // 从文件创建URL加载图片 img.src = URL.createObjectURL(file); }); } /** * 上传图片到图床 * @param {File} file - 要上传的图片文件 * @returns {Promise} - 返回上传后的图片URL */ async uploadImage(file) { const formData = new FormData(); formData.append("image", file); // 尝试使用主令牌 try { return await this.tryUploadWithToken( formData, this.settings.primaryToken, ); } catch (error) { console.warn("主令牌上传失败,尝试备用令牌", error); // 如果主令牌失败,尝试备用令牌 return await this.tryUploadWithToken( formData, this.settings.backupToken, ); } } /** * 使用特定令牌尝试上传 * @param {FormData} formData - 表单数据 * @param {string} token - 授权令牌 * @returns {Promise} - 返回上传后的图片URL */ async tryUploadWithToken(formData, token) { const response = await fetch(CONFIG.UPLOAD.URL, { method: "POST", headers: { "Auth-Token": token, }, body: formData, }); if (!response.ok) { throw new Error( `上传请求失败: ${response.status} ${response.statusText}`, ); } const data = await response.json(); if (!data.ok) { throw new Error(data.message || "服务器返回错误"); } return `${CONFIG.UPLOAD.BASE_URL}${data.src}`; } /** * 在编辑器中插入Markdown格式的图片链接 * @param {string} fileName - 文件名 * @param {string} imageMarkdown - 图片Markdown代码 * @returns {Object|null} - 返回插入位置信息 */ insertMarkdownImage(fileName, imageMarkdown) { // 尝试使用CodeMirror API插入内容 const editorElement = document.querySelector(CONFIG.SELECTORS.CODEMIRROR); if (editorElement?.CodeMirror) { // 使用CodeMirror API const cm = editorElement.CodeMirror; const cursor = cm.getCursor(); // 插入内容 cm.replaceRange(imageMarkdown + "\n", cursor); // 将光标移动到插入内容之后 cm.setCursor({ line: cursor.line + 1, ch: 0, }); // 聚焦编辑器 cm.focus(); // 返回插入位置 return { type: "codemirror", instance: cm, from: { line: cursor.line, ch: cursor.ch, }, to: { line: cursor.line, ch: cursor.ch + imageMarkdown.length, }, }; } // 如果无法获取CodeMirror实例,尝试使用textarea const textarea = document.querySelector(CONFIG.SELECTORS.TEXTAREA); if (textarea) { const startPos = textarea.selectionStart; const endPos = textarea.selectionEnd; // 插入内容 textarea.value = textarea.value.substring(0, startPos) + imageMarkdown + "\n" + textarea.value.substring(endPos); // 设置新的光标位置 textarea.selectionStart = textarea.selectionEnd = startPos + imageMarkdown.length + 1; // 聚焦输入框 textarea.focus(); // 返回插入位置 return { type: "textarea", element: textarea, start: startPos, end: startPos + imageMarkdown.length, }; } // 如果是插入多张图片的最后一张,确保光标回到编辑区末尾 const isLastImage = fileName.includes("__LAST__"); if (isLastImage && editorElement?.CodeMirror) { const cm = editorElement.CodeMirror; cm.setCursor(cm.lineCount(), 0); cm.focus(); } return null; } /** * 替换占位的Markdown代码 * @param {string} placeholderId - 占位符ID * @param {string} fileName - 文件名 * @param {string} imageUrl - 图片URL * @param {string} [customMarkdown] - 自定义Markdown代码 */ replaceMarkdownPlaceholder( placeholderId, fileName, imageUrl, customMarkdown = null, ) { // 获取文件名作为替代文本,去除扩展名 const altText = fileName.replace(/\.[^/.]+$/, ""); // 决定使用哪种Markdown代码 const newMarkdown = customMarkdown || `![${altText}](${imageUrl})`; // 尝试使用CodeMirror API替换内容 const editorElement = document.querySelector(CONFIG.SELECTORS.CODEMIRROR); if (editorElement?.CodeMirror) { const cm = editorElement.CodeMirror; const content = cm.getValue(); // 查找占位符 const placeholderRegex = new RegExp( `!\\[.*?\\]\\(uploading#${placeholderId}\\)`, "g", ); const match = placeholderRegex.exec(content); if (match) { const from = cm.posFromIndex(match.index); const to = cm.posFromIndex(match.index + match[0].length); // 替换占位符 cm.replaceRange(newMarkdown, from, to); return; } } // 如果无法使用CodeMirror或找不到占位符,尝试使用textarea const textarea = document.querySelector(CONFIG.SELECTORS.TEXTAREA); if (textarea) { const content = textarea.value; // 查找占位符 const placeholderRegex = new RegExp( `!\\[.*?\\]\\(uploading#${placeholderId}\\)`, "g", ); const match = placeholderRegex.exec(content); if (match) { // 替换占位符 textarea.value = content.substring(0, match.index) + newMarkdown + content.substring(match.index + match[0].length); } } } /** * 显示通知消息 * @param {string} message - 通知消息内容 * @param {string} type - 通知类型 ('success', 'error', 或 'info') * @param {number} [duration] - 通知显示时间(毫秒) */ showNotification( message, type, duration = CONFIG.UI.NOTIFICATION_DURATION, ) { // 删除同类型的现有通知(保留进度通知) const existingNotifications = document.querySelectorAll( `.ns-notification:not(.ns-progress-notification)`, ); existingNotifications.forEach((notification) => { document.body.removeChild(notification); }); // 创建新通知 const notification = document.createElement("div"); notification.className = `ns-notification ${type}`; notification.textContent = message; document.body.appendChild(notification); // 使通知可见 setTimeout(() => { notification.classList.add("visible"); }, 10); // 通知自动消失 setTimeout(() => { notification.classList.remove("visible"); // 等待过渡效果完成后删除元素 notification.addEventListener( "transitionend", () => { if (notification.parentNode) { document.body.removeChild(notification); } }, { once: true }, ); }, duration); } } // 初始化上传器 new ImageUploader(); // 在控制台显示版本信息 console.log("NodeSeek 图片上传工具 v1.5.0 已加载"); })();