// ==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,保护您的上传权限不被他人使用。
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 = ``;
// 插入占位符到编辑器
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",
``,
);
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",
``,
);
}
}
/**
* 显示进度通知
* @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 || ``;
// 尝试使用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 已加载");
})();