// ==UserScript== // @name [MT论坛]发帖辅助工具 // @namespace https://github.com/qcxs/mtbbs // @version 2026-05-06 // @description 寻找网页中textarea,为其添加菜单,核心功能插入标签和预览,另配有辅助工具。 // @author 青春向上 // @match *://bbs.binmt.cc/forum.php?*tid=* // @match *://bbs.binmt.cc/*thread-*.html* // @match *://bbs.binmt.cc/forum.php?*mod=post* // @require https://cdn.jsdelivr.net/gh/qcxs/mtbbs@master/require/BBCode2Html.js // @icon https://bbs.binmt.cc/favicon.ico // @grant none // ==/UserScript== (function () { 'use strict'; // 仅适配发帖页面 const textarea = document.querySelector('textarea'); if (!textarea) { console.log('未找到textarea元素'); return; } // 定义存储在localStorage中的键名 const QUICK_INPUT = 'QuickInput'; const MEUN_ITEMS = 'MenuItems'; const CONNECT_CACHE = 'Connect_Cache'; const DEFAULT = [] const localStorageTool = { //初始化key为target的localStorage initlocalStorage(target) { localStorage.setItem(target, JSON.stringify(DEFAULT[target])); return this.getBylocalStorage(target); }, // 获取key为target的localStorage的值并转为变量 getBylocalStorage(target) { try { // 为空重置数据 return JSON.parse(localStorage.getItem(target) || this.initlocalStorage(target)); } catch (error) { // 损坏重置数据 return this.initlocalStorage(target); } } } DEFAULT[QUICK_INPUT] = [ { name: "添加", value: "add" }, { name: "删除", value: "delete" } ] DEFAULT[MEUN_ITEMS] = [ { name: '↶', isShow: true }, { name: '↷', isShow: true }, { name: '选择', isShow: true }, { name: '预览', isShow: true }, { name: '插图', isShow: true }, { name: '常用语', isShow: true }, { name: '彩虹字体', isShow: true }, { name: 'Eruda', isShow: false }, { name: '自定义菜单', isShow: false }, { name: '更多', isShow: true }, //默认隐藏,点击更多显示 { name: '移除标签', isShow: false }, { name: '阻止离开', isShow: false }, { name: '原布局', isShow: false }, { name: '帖子缓存', isShow: false }, ] let isRemoveTouchStart = false; let ErudaVisible = false; // 封装创建函数 function createMenu() { const menu = document.createElement('div'); menu.style.cssText = ` display: flex; overflow-y: hidden; padding: 4px; background: #fff; border: 1px solid #ddd; gap: 4px; `; return menu; } function createItem(name, callback) { const btn = document.createElement('a'); btn.textContent = name; btn.style.cssText = ` padding: 2px 8px; font-size: 12px; border: 1px solid #ccc; border-radius: 2px; background: #f5f5f5; white-space: nowrap; `; btn.addEventListener('click', () => callback(name)); return btn; } // 创建菜单容器 const menu = createMenu(); let menuItems = ['B', 'S', '𝐼', 'U', 'url', 'hr', 'code', 'media', 'color', 'size', 'img', 'hide', 'align', 'email', 'quote', 'QQ', 'backcolor'] // 渲染菜单 menu.innerHTML = ''; menuItems.forEach(item => { const btn = createItem(item, bbcodeHandleAction) menu.appendChild(btn); }); textarea.parentNode.insertBefore(menu, textarea.nextSibling); // 创建菜单容器 const functionMenu = createMenu() // 从localStorage中读取菜单 let functionMenuItems = localStorageTool.getBylocalStorage(MEUN_ITEMS); // 渲染菜单,isShow控制是否显示 functionMenu.innerHTML = ''; functionMenuItems.filter(item => item.isShow).forEach(item => { const btn = createItem(item.name, functionHandleAction) functionMenu.appendChild(btn); }); // 将菜单插入到textarea后面 textarea.parentNode.insertBefore(functionMenu, textarea.previousSibling); // 添加间距样式 functionMenu.style.marginTop = '4px'; // 使用Switch处理菜单操作 function functionHandleAction(name) { // 获取选中文本信息 const textarea = document.querySelector('textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); // 使用switch处理不同操作 switch (name) { case '↶': { textarea.focus(); document.execCommand('undo'); } break; case '↷': { textarea.focus(); document.execCommand('redo'); } break; case '常用语': selectDia(localStorageTool.getBylocalStorage(QUICK_INPUT), (value) => { if (value === 'add') { inputDia([ { placeholder: '请输入备注:' }, { placeholder: '请输入内容:' }, ], (name, value) => { if (name.trim() && value.trim()) { // 向前面添加 const arr = localStorageTool.getBylocalStorage(QUICK_INPUT); arr.unshift({ name, value }); localStorage.setItem(QUICK_INPUT, JSON.stringify(arr)); } }); } else if (value === 'delete') { selectDia(localStorageTool.getBylocalStorage(QUICK_INPUT), (value) => { // 删除(不能删除"添加"和"删除") if (value === "add" || value === "delete") return false; const arr = localStorageTool.getBylocalStorage(QUICK_INPUT); const newArr = arr.filter(item => item.value !== value); if (newArr.length < arr.length) { localStorage.setItem(QUICK_INPUT, JSON.stringify(newArr)); } }, "请选择要删除的常用语:"); } else { insertTextAndScroll(`${selectedText}${value}`); } }, "请选择常用语/操作:"); break; case '预览': if (BBCode2Html == null) { alert('核心BBCode2Html未加载,请查看require链接是否正确') return; } BBCode2Html.show(textarea.value); break; case '选择': selectIncludingBrackets(); break; case '彩虹字体': rainbowTextGenerator(`${selectedText}`, (code) => { insertTextAndScroll(`${code}`); }); break; case '插图': pictureManagementTool() break; case '自定义菜单': { // 读取本地缓存 const cache = localStorage.getItem(MEUN_ITEMS) || ''; // 弹窗输入 inputDia([{ placeholder: '菜单json:', value: cache }], (json) => { // 写入本地存储 localStorage.setItem(MEUN_ITEMS, json.trim()); alert('刷新网页生效'); }); } break; case '更多': const newArr = functionMenuItems .filter(item => !item.isShow) .map(item => ({ name: item.name, value: item.name })); selectDia(newArr, (name) => { functionHandleAction(name) }, '隐藏菜单内容:') break; case '移除标签': { inputDia([ { placeholder: '例如:输入color移除所有彩色字体', value: "" }, ], (lable) => { // 转义name中的特殊字符,避免正则表达式语法错误 const escapedName = lable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\[(?:\\/)?${escapedName}(?:=.*?)?\\]`, 'gi'); // 替换匹配的内容为空字符串 textarea.value = textarea.value.replace(regex, ''); }) } break; case '阻止离开': { window.addEventListener('beforeunload', function (e) { e.preventDefault(); e.returnValue = ''; // 这行在某些浏览器中是必需的 return '您有未保存的内容,确定要离开吗?'; }); alert('已阻止离开,网页跳转或刷新,将被阻止') } break; case '原布局': { const imgList = document.querySelector('#imglist'); imgList.style.display = imgList.style.display == 'block' ? 'none' : 'block' } break; case '帖子缓存': { const cache = localStorage.getItem(CONNECT_CACHE) || '无缓存' inputDia([{ placeholder: '缓存内容:(预览)', value: cache }], () => { insertTextAndScroll(cache); }) } break; case 'Eruda': { // 如果已加载过 eruda,直接切换显示状态 if (window.eruda) { ErudaVisible = !ErudaVisible; // 取反状态 ErudaVisible ? eruda.init() : eruda.destroy(); // 显示/隐藏 return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/eruda'; // 使用原始src(保留参数) script.type = 'text/javascript'; // 加载成功回调 script.onload = function () { eruda.init(); // 初始化 ErudaVisible = true; }; // 添加到页面 document.head.appendChild(script); } break; default: alert(`未知操作${name}`); return; } } // 使用Switch处理菜单操作 function bbcodeHandleAction(name) { //首次点击按钮时,禁用侧边栏 if (!isRemoveTouchStart) { try { window.$('html').off('touchstart'); // 打开侧边栏 comiis_leftnv() console.log('移除滑动打开侧边栏') } catch (error) { console.log('可能没有侧边栏', error) } // 每30秒,将textarea中内容保存在缓存中 setInterval(() => { const textarea = document.querySelector('textarea'); if (textarea.value.length > 200) { localStorage.setItem(CONNECT_CACHE, textarea.value) console.log('缓存帖子内容。') } }, 30000) isRemoveTouchStart = true; } // 获取选中文本信息 const textarea = document.querySelector('textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); // 使用switch处理不同操作 switch (name) { case 'B': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入文字:' }], (text) => { insertTextAndScroll(`[b]${text}[/b]`); }) return; } insertTextAndScroll(`[b]${selectedText}[/b]`); break; case '𝐼': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入文字:' }], (text) => { insertTextAndScroll(`[i]${text}[/i]`); }) return; } insertTextAndScroll(`[i]${selectedText}[/i]`); break; case 'U': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入文字:' }], (text) => { insertTextAndScroll(`[u]${text}[/u]`); }) return; } insertTextAndScroll(`[u]${selectedText}[/u]`); break; case 'S': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入文字:' }], (text) => { insertTextAndScroll(`[s]${text}[/s]`); }) return; } insertTextAndScroll(`[s]${selectedText}[/s]`); break; case 'size': if (!selectedText.trim()) { inputDia([ { placeholder: '请输入size显示的文字:' } ], (text) => { selectDia([ { name: '1号文字', value: `[size=1]${text}[/size]`, css: "font-size: 8px;" }, { name: '2号文字', value: `[size=2]${text}[/size]`, css: "font-size: 10px;" }, { name: '3号文字', value: `[size=3]${text}[/size]`, css: "font-size: 12px;" }, { name: '4号文字', value: `[size=4]${text}[/size]`, css: "font-size: 14px;" }, { name: '5号文字', value: `[size=5]${text}[/size]`, css: "font-size: 18px;" }, { name: '6号文字', value: `[size=6]${text}[/size]`, css: "font-size: 24px;" }, { name: '7号文字', value: `[size=7]${text}[/size]`, css: "font-size: 35px;" }, { name: '8号文字', value: `[size=8]${text}[/size]`, css: "font-size: 35px;" }, { name: '9号文字', value: `[size=9]${text}[/size]`, css: "font-size: 35px;" } ], (value) => { insertTextAndScroll(`${value}`); }, "请选择字号:"); }) return; } insertTextAndScroll(`[size=]${selectedText}[/size]`); break; case 'color': case 'backcolor': const text = name === 'color' ? 'color' : 'backcolor'; const textText = name === 'color' ? 'color' : 'background'; selectDia([ //选项名、插入内容、预览样式(css) { name: '取色器', value: `取色器` }, { name: `black`, value: `[${text}=black]${selectedText}[/${text}]`, css: `${textText}: black;` }, { name: `white`, value: `[${text}=white]${selectedText}[/${text}]`, css: `${textText}: white;` }, { name: `red`, value: `[${text}=red]${selectedText}[/${text}]`, css: `${textText}: red;` }, { name: `green`, value: `[${text}=green]${selectedText}[/${text}]`, css: `${textText}: green;` }, { name: `blue`, value: `[${text}=blue]${selectedText}[/${text}]`, css: `${textText}: blue;` }, { name: `yellow`, value: `[${text}=yellow]${selectedText}[/${text}]`, css: `${textText}: yellow;` }, { name: `purple`, value: `[${text}=purple]${selectedText}[/${text}]`, css: `${textText}: purple;` }, { name: `orange`, value: `[${text}=orange]${selectedText}[/${text}]`, css: `${textText}: orange;` }, { name: `pink`, value: `[${text}=pink]${selectedText}[/${text}]`, css: `${textText}: pink;` }, { name: `gray`, value: `[${text}=gray]${selectedText}[/${text}]`, css: `${textText}: gray;` }, { name: `brown`, value: `[${text}=brown]${selectedText}[/${text}]`, css: `${textText}: brown;` }, { name: `cyan`, value: `[${text}=cyan]${selectedText}[/${text}]`, css: `${textText}: cyan;` }, { name: `magenta`, value: `[${text}=magenta]${selectedText}[/${text}]`, css: `${textText}: magenta;` }, { name: `olive`, value: `[${text}=olive]${selectedText}[/${text}]`, css: `${textText}: olive;` }, { name: `teal`, value: `[${text}=teal]${selectedText}[/${text}]`, css: `${textText}: teal;` }, { name: `navy`, value: `[${text}=navy]${selectedText}[/${text}]`, css: `${textText}: navy;` }, { name: `maroon`, value: `[${text}=maroon]${selectedText}[/${text}]`, css: `${textText}: maroon;` }, { name: `silver`, value: `[${text}=silver]${selectedText}[/${text}]`, css: `${textText}: silver;` }, { name: `gold`, value: `[${text}=gold]${selectedText}[/${text}]`, css: `${textText}: gold;` } ], (value) => { if (value === '取色器') { // 调用取色器选择颜色并处理 showColorPicker((hex) => { insertTextAndScroll(`[${text}=${hex}]${selectedText}[/${text}]`); }); } else { insertTextAndScroll(`${value}`); } }, "请选择颜色:"); break; case 'url': if (!selectedText.trim()) { inputDia([ { placeholder: '请输入文字:' }, { placeholder: '请输入链接:' }, ], (text, url) => { if (!url.trim()) { insertTextAndScroll(`[url]${text}[/url]`); } else { insertTextAndScroll(`[url=${url}]${text}[/url]`); } }) return; } // 判断是否为http/https链接 const isLink = /^https?:\/\/.+/.test(selectedText); if (isLink) { insertTextAndScroll(`[url]${selectedText}[/url]`); } else { insertTextAndScroll(`[url=]${selectedText}[/url]`); } break; case 'email': if (!selectedText.trim()) { inputDia([ { placeholder: '请输入文字:' }, { placeholder: '请输入邮箱:' }, ], (text, email) => { if (!email.trim()) { insertTextAndScroll(`[email]${text}[/email]`); } else { insertTextAndScroll(`[email=${email}]${text}[/email]`); } }) return; } insertTextAndScroll(`[email=${selectedText}]${selectedText}[/email]`); break; case 'img': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入图片链接:' }], (img) => { insertTextAndScroll(`[img]${img}[/img]`); }) return; } insertTextAndScroll(`[img]${selectedText}[/img]`); break; case 'hr': insertTextAndScroll(`[hr]${selectedText}`); break; case 'align': selectDia([ { name: '左对齐', value: `[align=left]${selectedText}[/align]`, css: "justify-content: flex-start;" }, { name: '居中', value: `[align=center]${selectedText}[/align]`, css: "justify-content: center;" }, { name: '右对齐', value: `[align=right]${selectedText}[/align]`, css: "justify-content: flex-end;" }, ], (value) => { insertTextAndScroll(`${value}`); }, "请选择对齐方式:"); break; case 'code': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入code:' }], (code) => { insertTextAndScroll(`[code]${code}[/code]`); }) return; } insertTextAndScroll(`[code]${selectedText}[/code]`); break; case 'quote': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入引用:' }], (img) => { insertTextAndScroll(`[quote]${img}[/quote]`); }) return; } insertTextAndScroll(`[quote]${selectedText}[/quote]`); break; case 'hide': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入隐藏内容:' }], (img) => { insertTextAndScroll(`[hide]${img}[/hide]`); }) return; } insertTextAndScroll(`[hide]${selectedText}[/hide]`); break; case 'media': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入视频链接:' }], (img) => { insertTextAndScroll(`[media=x,500,375]${img}[/media]`); }) return; } insertTextAndScroll(`[media=x,500,375]${selectedText}[/media]`); break; case 'QQ': if (!selectedText.trim()) { inputDia([{ placeholder: '请输入QQ号:' }], (img) => { insertTextAndScroll(`[qq]${img}[/qq]`); }) return; } insertTextAndScroll(`[qq]${selectedText}[/qq]`); break; default: alert(`未知操作${name}`); return; } } // 插入文本并滚动至光标位置,且选中插入的文字 function insertTextAndScroll(text) { // 论坛字符上限,插入大量字符也会卡死,取消插入。 if (text.length > 20000) { alert('你插入的字符数已超过20000,已取消本次插入!') return; } const el = document.querySelector('textarea'); if (!el || !text) return; // 增加容错 el.focus(); // 聚焦元素 const startPos = el.selectionStart; // 记录插入前的光标起始位置 const endPos = el.selectionEnd; // 记录插入前的光标结束位置(处理原有选中内容) // 执行插入文本命令 const isSuccess = document.execCommand('insertText', false, text); // 若execCommand失败,手动处理文本插入和选区 if (!isSuccess) { const currentValue = el.value; // 替换原有选中内容为新文本 const newValue = currentValue.substring(0, startPos) + text + currentValue.substring(endPos); el.value = newValue; } // 调整选区为插入的文本 el.selectionStart = startPos; el.selectionEnd = startPos + text.length; // textarea内部滚动(简易版:按行号粗略滚动) if (el.tagName === 'TEXTAREA') { const caretPos = el.selectionEnd; // 用选区结束位置计算行号 const rowCount = el.value.slice(0, caretPos).split('\n').length; // 光标行号 // 动态获取行高(更精准) const lineHeight = parseInt(getComputedStyle(el).lineHeight) || 20; el.scrollTop = rowCount * lineHeight - el.clientHeight / 2; // 居中行 } } // 选择弹窗 function selectDia(options, callback, title = "请选择") { // 生成唯一ID const dialogId = `select-dialog-${Date.now()}`; // 创建容器并拼接DOM结构 const container = document.createElement('div'); // 动态添加选中样式的CSS(避免全局样式污染) const style = document.createElement('style'); style.textContent = ` .select-dialog-selected { background: #e6f3ff !important; /* 选中样式,!important确保优先级 */ } `; document.head.appendChild(style); container.innerHTML = `
${title}
    ${options.map((opt, i) => `
  • `).join('')}
`; // 阻止弹窗内部点击冒泡到遮罩层 container.querySelector('.dialog').addEventListener('click', e => e.stopPropagation()); // 确定按钮点击事件 const confirmBtn = container.querySelector('.confirm-btn'); confirmBtn.addEventListener('click', () => { const selectedRadio = container.querySelector(`input[name="${dialogId}"]:checked`); if (selectedRadio && typeof callback === 'function') { callback(selectedRadio.value); } container.remove(); document.head.removeChild(style); // 移除动态添加的样式,避免内存泄漏 }); // 添加到页面 document.body.appendChild(container); } // 动态输入框,可以定义输入个数 function inputDia(inputConfigs, callback) { // 创建遮罩层 const mask = document.createElement('div'); mask.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;overflow:auto'; // 构建对话框核心HTML(添加标题区域,调整flex布局) mask.innerHTML = `
动态输入框
`; // 生成可编辑div(添加outline:none移除焦点高亮) const inputContainer = mask.querySelector('#inputContainer'); inputConfigs.forEach(config => { const div = document.createElement('div'); div.innerHTML = ``; const editDiv = document.createElement('div'); editDiv.contentEditable = true; editDiv.style.cssText = 'min-height:80px;padding:10px;border:1px solid #ccc;margin-top:8px;outline:none;'; editDiv.textContent = config.value || ''; div.appendChild(editDiv); inputContainer.appendChild(div); }); // 遮罩层点击关闭 mask.addEventListener('click', e => e.target === mask && mask.remove()); // 取消点击事件 mask.querySelector('.CacheBtn').addEventListener('click', e => mask.remove()); // 确定按钮事件 → 已修复换行丢失问题 mask.querySelector('.OKBtn').addEventListener('click', e => { const container = mask.querySelector('#inputContainer'); // ✅ 修复:使用 innerText 保留换行符 const values = Array.from(container.querySelectorAll('[contenteditable]')).map(el => el.innerText); callback(...values); mask.remove(); }); document.body.appendChild(mask); } // 取色器 function showColorPicker(callback) { // 生成唯一ID避免冲突 const pickerId = `color-picker-${Date.now()}`; // 创建主容器并通过innerHTML一次性构建所有结构 const container = document.createElement('div'); container.innerHTML = `

选择颜色

5
50
255
`; // 添加到页面 document.body.appendChild(container); // 获取DOM元素引用 const overlay = document.getElementById(`${pickerId}-overlay`); const pickerContainer = document.getElementById(`${pickerId}-container`); const closeBtn = pickerContainer.querySelector('.close-btn'); const hueSlider = pickerContainer.querySelector('.hue-slider'); const hueHandle = pickerContainer.querySelector('.hue-handle'); const colorPanel = pickerContainer.querySelector('.color-panel'); const colorHandle = pickerContainer.querySelector('.color-handle'); const hexInput = document.getElementById(`${pickerId}-hex`); const rSlider = document.getElementById(`${pickerId}-r`); const gSlider = document.getElementById(`${pickerId}-g`); const bSlider = document.getElementById(`${pickerId}-b`); const rValue = document.getElementById(`${pickerId}-r-value`); const gValue = document.getElementById(`${pickerId}-g-value`); const bValue = document.getElementById(`${pickerId}-b-value`); const confirmBtn = pickerContainer.querySelector('.confirm-btn'); const cancelBtn = pickerContainer.querySelector('.cancel-btn'); // 颜色变量 let h = 251; // 初始色相(对应#0532ff) let s = 98; // 初始饱和度 let l = 51; // 初始亮度 let savedHexValue = hexInput.value; // 保存初始Hex值 // 移除弹窗的函数 function removePicker() { container.remove(); } // 关闭按钮事件 closeBtn.addEventListener('click', removePicker); cancelBtn.addEventListener('click', removePicker); // 确定按钮事件 confirmBtn.addEventListener('click', () => { if (typeof callback === 'function') { callback(hexInput.value); } removePicker(); }); // HSL转RGB function hslToRgb(h, s, l) { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } // RGB转HSL function rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { const delta = max - min; s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min); switch (max) { case r: h = (g - b) / delta + (g < b ? 6 : 0); break; case g: h = (b - r) / delta + 2; break; case b: h = (r - g) / delta + 4; break; } h /= 6; } h = Math.round(h * 360); s = Math.round(s * 100); l = Math.round(l * 100); return [h, s, l]; } // 绘制颜色面板 function drawColorPanel() { const displayWidth = colorPanel.offsetWidth; const displayHeight = colorPanel.offsetHeight; const canvasWidth = colorPanel.width; const canvasHeight = colorPanel.height; const scaleX = canvasWidth / displayWidth; const scaleY = canvasHeight / displayHeight; const ctx = colorPanel.getContext('2d'); const imageData = ctx.createImageData(canvasWidth, canvasHeight); const data = imageData.data; for (let y = 0; y < canvasHeight; y++) { for (let x = 0; x < canvasWidth; x++) { const sVal = x / canvasWidth; const lVal = (canvasHeight - y) / canvasHeight; const [r, g, b] = hslToRgb(h, sVal * 100, lVal * 100); const i = (y * canvasWidth + x) * 4; data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = 255; } } ctx.putImageData(imageData, 0, 0); } // 更新色相滑块位置 function updateHueHandlePosition() { const height = hueSlider.offsetHeight; const top = height - (h / 360) * height; hueHandle.style.top = `${top}px`; } // 更新颜色手柄位置 function updateColorHandlePosition() { const width = colorPanel.offsetWidth; const height = colorPanel.offsetHeight; const handleSize = 20; const x = (s / 100) * width - handleSize / 2; const y = height - (l / 100) * height - handleSize / 2; colorHandle.style.left = `${x}px`; colorHandle.style.top = `${y}px`; } // 更新RGB滑块和显示 function updateRGBSliders(r, g, b) { rSlider.value = r; rValue.textContent = r; gSlider.value = g; gValue.textContent = g; bSlider.value = b; bValue.textContent = b; } // 更新Hex输入框 function updateHexInput(r, g, b) { const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; hexInput.value = hex; savedHexValue = hex; } // 从HSL更新界面 function updateFromHSL() { const [r, g, b] = hslToRgb(h, s, l); updateRGBSliders(r, g, b); updateHexInput(r, g, b); drawColorPanel(); updateColorHandlePosition(); } // 从RGB更新界面 function updateFromRGB() { const r = parseInt(rSlider.value); const g = parseInt(gSlider.value); const b = parseInt(bSlider.value); [h, s, l] = rgbToHsl(r, g, b); updateHueHandlePosition(); drawColorPanel(); updateColorHandlePosition(); updateHexInput(r, g, b); } // 从Hex更新界面 function updateFromHex(hex) { hex = hex.replace(/^#/, ''); if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); if (hex.length !== 6) hex = '0532ff'; const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); updateRGBSliders(r, g, b); updateFromRGB(); } // Hex输入框事件处理 hexInput.addEventListener('focus', () => { savedHexValue = hexInput.value; }); hexInput.addEventListener('input', (e) => { const value = e.target.value; const filtered = value.replace(/[^#0-9a-fA-F]/g, ''); const hashIndex = filtered.indexOf('#'); if (hashIndex > 0) { const withoutHash = filtered.replace(/#/g, ''); e.target.value = '#' + withoutHash.substring(0, 6); } else if (hashIndex === 0) { e.target.value = filtered.substring(0, 7); } else { e.target.value = filtered.substring(0, 6); } const currentValue = e.target.value; if ((currentValue.length === 7 && currentValue.startsWith('#')) || (currentValue.length === 6 && !currentValue.startsWith('#'))) { updateFromHex(currentValue); } }); hexInput.addEventListener('blur', () => { const value = hexInput.value.replace(/^#/, '').toUpperCase(); if (/^[0-9A-F]{6}$/.test(value)) { hexInput.value = '#' + value; updateFromHex(hexInput.value); } else { hexInput.value = savedHexValue; } }); // 监听色相条拖动 let isHueDragging = false; const handleHueDrag = (e) => { if (!isHueDragging) return; e.preventDefault(); const rect = hueSlider.getBoundingClientRect(); let y = e.type.includes('touch') ? e.touches[0].clientY - rect.top : e.clientY - rect.top; h = 360 - (y / rect.height) * 360; h = Math.max(0, Math.min(360, h)); updateHueHandlePosition(); updateFromHSL(); }; hueSlider.addEventListener('mousedown', (e) => { isHueDragging = true; handleHueDrag(e); }); hueSlider.addEventListener('touchstart', (e) => { isHueDragging = true; handleHueDrag(e); }); document.addEventListener('mousemove', handleHueDrag); document.addEventListener('touchmove', handleHueDrag); document.addEventListener('mouseup', () => isHueDragging = false); document.addEventListener('touchend', () => isHueDragging = false); // 监听颜色面板拖动 let isColorDragging = false; const handleColorDrag = (e) => { if (!isColorDragging) return; e.preventDefault(); const rect = colorPanel.getBoundingClientRect(); let x = e.type.includes('touch') ? e.touches[0].clientX - rect.left : e.clientX - rect.left; let y = e.type.includes('touch') ? e.touches[0].clientY - rect.top : e.clientY - rect.top; s = (x / rect.width) * 100; l = ((rect.height - y) / rect.height) * 100; s = Math.max(0, Math.min(100, s)); l = Math.max(0, Math.min(100, l)); updateColorHandlePosition(); updateFromHSL(); }; colorPanel.addEventListener('mousedown', (e) => { isColorDragging = true; handleColorDrag(e); }); colorPanel.addEventListener('touchstart', (e) => { isColorDragging = true; handleColorDrag(e); }); document.addEventListener('mousemove', handleColorDrag); document.addEventListener('touchmove', handleColorDrag); document.addEventListener('mouseup', () => isColorDragging = false); document.addEventListener('touchend', () => isColorDragging = false); // 监听RGB滑块变化 rSlider.addEventListener('input', updateFromRGB); gSlider.addEventListener('input', updateFromRGB); bSlider.addEventListener('input', updateFromRGB); // 响应式调整 function adjustForScreenSize() { const containerWidth = pickerContainer.offsetWidth; const colorSelection = pickerContainer.querySelector('.color-selection'); if (containerWidth < 350) { colorSelection.style.flexDirection = 'column'; hueSlider.style.width = '100%'; hueSlider.style.height = '30px'; hueSlider.style.background = 'linear-gradient(to right, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(360, 100%, 50%))'; hueHandle.style.width = '12px'; hueHandle.style.height = '40px'; hueHandle.style.left = '0'; hueHandle.style.top = '-5px'; updateHueHandlePosition = () => { const width = hueSlider.offsetWidth; const left = (h / 360) * width; hueHandle.style.left = `${left}px`; }; } else { colorSelection.style.flexDirection = 'row'; hueSlider.style.width = '30px'; hueSlider.style.height = '240px'; hueSlider.style.background = 'linear-gradient(to top, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(360, 100%, 50%))'; hueHandle.style.width = '40px'; hueHandle.style.height = '12px'; hueHandle.style.left = '-5px'; hueHandle.style.top = '0'; updateHueHandlePosition = () => { const height = hueSlider.offsetHeight; const top = height - (h / 360) * height; hueHandle.style.top = `${top}px`; }; } drawColorPanel(); updateHueHandlePosition(); } // 初始化 updateFromHex(hexInput.value); updateHueHandlePosition(); drawColorPanel(); updateColorHandlePosition(); adjustForScreenSize(); // 窗口大小变化监听 window.addEventListener('resize', adjustForScreenSize); // 移除弹窗时清理事件 const originalRemovePicker = removePicker; removePicker = () => { window.removeEventListener('resize', adjustForScreenSize); originalRemovePicker(); }; } // 彩虹字体生成器,待彩虹的字符串、回调函数(彩虹代码) function rainbowTextGenerator(initialText, callback) { if (!initialText.trim()) initialText = '彩虹字体示例'; // 创建弹窗元素 const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; `; // 弹窗内容 overlay.innerHTML = `

彩虹字体生成

360°
30° 120°
0% 80% 100%
10% 50% 90%
`; document.body.appendChild(overlay); // 获取元素 const container = overlay.querySelector('.content-container'); const input = overlay.querySelector('.input-area'); const preview = overlay.querySelector('.preview-area'); const hueStart = overlay.querySelector('.hue-start'); const hueStep = overlay.querySelector('.hue-step'); const saturation = overlay.querySelector('.saturation'); const lightness = overlay.querySelector('.lightness'); const hueStartValue = overlay.querySelector('.hue-start-value'); const hueStepValue = overlay.querySelector('.hue-step-value'); const saturationValue = overlay.querySelector('.saturation-value'); const lightnessValue = overlay.querySelector('.lightness-value'); const randomBtn = overlay.querySelector('.random-btn'); const cancelBtn = overlay.querySelector('.cancel-btn'); const confirmBtn = overlay.querySelector('.confirm-btn'); // 颜色转换工具 const hslToRgb = (h, s, l) => { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; } else { const f = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = f(p, q, h + 1 / 3); g = f(p, q, h); b = f(p, q, h - 1 / 3); } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; }; const rgbToHex = (r, g, b) => `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; // 生成彩虹文字 const generate = () => { const text = input.value.trim(); if (!text) { preview.innerHTML = '点击此处输入文字内容'; return ''; } const scrollTop = input.scrollTop; let html = ''; let code = ''; const start = parseInt(hueStart.value); const step = parseInt(hueStep.value); const sat = parseInt(saturation.value); const lit = parseInt(lightness.value); for (let i = 0; i < text.length; i++) { const hue = (start + i * step) % 360; const rgb = hslToRgb(hue, sat, lit); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); html += `${text[i]}`; code += `[color=${hex}]${text[i]}[/color]`; } preview.innerHTML = html; preview.scrollTop = scrollTop; return code; }; // 更新显示值 const updateValues = () => { hueStartValue.textContent = `${hueStart.value}°`; hueStepValue.textContent = `${hueStep.value}°`; saturationValue.textContent = `${saturation.value}%`; lightnessValue.textContent = `${lightness.value}%`; }; // 切换显示模式 const showInput = () => { preview.style.display = 'none'; input.style.display = 'block'; input.focus(); input.scrollTop = preview.scrollTop; }; const showPreview = () => { if (input.style.display === 'block') { input.style.display = 'none'; preview.style.display = 'block'; generate(); } }; // 随机颜色 const randomize = () => { hueStart.value = Math.floor(Math.random() * 360); hueStep.value = Math.floor(Math.random() * 60) + 10; saturation.value = Math.floor(Math.random() * 50) + 50; lightness.value = Math.floor(Math.random() * 40) + 30; updateValues(); generate(); }; // 事件绑定 container.addEventListener('click', () => showInput()); input.addEventListener('blur', showPreview); input.addEventListener('input', () => preview.scrollTop = input.scrollTop); hueStart.addEventListener('input', () => { updateValues(); generate(); }); hueStep.addEventListener('input', () => { updateValues(); generate(); }); saturation.addEventListener('input', () => { updateValues(); generate(); }); lightness.addEventListener('input', () => { updateValues(); generate(); }); randomBtn.addEventListener('click', () => { showPreview(); randomize(); }); cancelBtn.addEventListener('click', () => document.body.removeChild(overlay)); confirmBtn.addEventListener('click', () => { showPreview(); const code = generate(); callback(code); document.body.removeChild(overlay); }); // 初始化 updateValues(); generate(); } let currentImgIndex = 0; // 图片管理工具,可以插图、上传图片、删除图片 function pictureManagementTool() { const textarea = document.querySelector('textarea'); let images = []; // 全局维护图片列表 let overlay = null; // 遮罩层实例 let popupObserver = null; // 弹窗监听实例(全局单例) // 收集图片(ID改为匹配数字) const collectTargetImages = () => { const idPattern = /^aimg_(\d+)$/; const images = Array.from(document.querySelectorAll('#imglist img') || []).reduce((acc, img) => { const match = img.id.match(idPattern); if (match) acc.push({ id: img.id, src: img.src, number: match[1], imgNode: img }); return acc; }, []); return images.reverse(); }; // 获取已插入的图片编号(ID改为数字) const getInsertedNumbers = () => { const pattern = /\[attachimg\](\d+)\[\/attachimg\]/g; const inserted = []; let match; while ((match = pattern.exec(textarea.value))) inserted.push(match[1]); return inserted; }; // 插入到文本框 const insertIntoTextarea = (number) => { const { selectionStart: start, selectionEnd: end } = textarea; const insertStr = `[attachimg]${number}[/attachimg]`; textarea.value = textarea.value.slice(0, start) + insertStr + textarea.value.slice(end); textarea.focus(); textarea.setSelectionRange(start + insertStr.length, start + insertStr.length); }; // 创建大图预览区域(含全屏按钮) const createMainPreview = (src) => { return `
`; }; // 更新缩略图列表 const updateThumbnailList = () => { if (!overlay) return; images = collectTargetImages(); const insertedNumbers = getInsertedNumbers(); const thumbnailContainer = overlay.querySelector('.thumbnail-list'); const previewArea = overlay.querySelector('.preview-area'); const hasImages = images.length > 0; // 渲染缩略图 thumbnailContainer.innerHTML = hasImages ? images.map((img, i) => `
缩略图${i + 1} ${insertedNumbers.includes(img.number) ? '已插' : ''}
`).join('') : '
暂无
'; // 处理大图预览 if (hasImages) { // 校验当前索引 if (currentImgIndex === undefined || currentImgIndex >= images.length || currentImgIndex < 0) { currentImgIndex = 0; } const currentImg = images[currentImgIndex]; // 生成大图区域HTML(含全屏按钮) previewArea.innerHTML = createMainPreview(currentImg.src); // 绑定全屏按钮事件 const fullscreenBtn = previewArea.querySelector('#fullscreenBtn'); fullscreenBtn.onclick = () => { window.open(currentImg.src, '_blank'); }; } else { // 无图片时显示提示 previewArea.innerHTML = '
请先上传图片
'; currentImgIndex = 0; } // 绑定缩略图点击事件 if (hasImages) { thumbnailContainer.querySelectorAll('[data-index]').forEach(thumb => { thumb.onclick = () => { const index = +thumb.dataset.index; currentImgIndex = index; const currentImg = images[index]; const previewArea = overlay.querySelector('.preview-area'); // 更新大图和全屏按钮 previewArea.innerHTML = createMainPreview(currentImg.src); const fullscreenBtn = previewArea.querySelector('#fullscreenBtn'); fullscreenBtn.onclick = () => { window.open(currentImg.src, '_blank'); }; }; }); // 绑定移除按钮点击事件 thumbnailContainer.querySelectorAll('.img-remove-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const number = btn.dataset.number; const delElement = document.querySelector(`#imglist span[aid="${number}"]`); if (delElement) delElement.click(); // 重新更新列表 updateThumbnailList(); }; }); } // 更新插入按钮显示 const insertBtn = overlay.querySelector('#insertBtn'); if (insertBtn) { insertBtn.style.display = hasImages ? 'inline-block' : 'none'; } }; // 初始化监听(仅首次调用) const initUploadObserver = () => { if (popupObserver) return; // 已存在监听器,直接返回 const imgList = document.querySelector('#imglist'); if (!imgList) return; popupObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const hasImgChange = Array.from(mutation.addedNodes).some(node => node.tagName === 'LI') || Array.from(mutation.removedNodes).some(node => node.tagName === 'LI'); if (hasImgChange) updateThumbnailList(); }); }); const observerOptions = { childList: true, subtree: true }; popupObserver.observe(imgList, observerOptions); }; // 创建遮罩层(单例) const createOverlay = () => { const existingOverlay = document.getElementById('imageHelperOverlay'); if (existingOverlay) { overlay = existingOverlay; return; // 已有实例,直接返回 } const overlayHtml = `

图片插入助手

`; document.body.insertAdjacentHTML('beforeend', overlayHtml); overlay = document.getElementById('imageHelperOverlay'); // 绑定按钮事件(仅首次创建时绑定) const cancelBtn = overlay.querySelector('#cancelBtn'); const insertBtn = overlay.querySelector('#insertBtn'); const uploadBtn = overlay.querySelector('#uploadBtn'); uploadBtn.onclick = () => { const uploadInput = document.querySelector('#imglist input') || document.querySelector('#filedata'); if (uploadInput) uploadInput.click(); }; insertBtn.onclick = () => { if (images.length > 0) { insertIntoTextarea(images[currentImgIndex || 0].number); overlay.style.display = 'none'; // 隐藏而非删除 } }; cancelBtn.onclick = () => { overlay.style.display = 'none'; // 隐藏而非删除 }; overlay.onclick = (e) => { if (e.target === overlay) { overlay.style.display = 'none'; // 隐藏而非删除 } }; // 首次创建遮罩层时初始化监听器(仅一次) initUploadObserver(); }; // 打开/显示图片助手(单例逻辑) const openImageHelper = () => { createOverlay(); // 确保实例存在(首次创建时初始化监听器) overlay.style.display = 'flex'; // 显示遮罩层 updateThumbnailList(); // 先更新列表(包含大图初始化) }; openImageHelper(); } //选择 function selectIncludingBrackets() { const textarea = document.querySelector('textarea'); const text = textarea.value; const selectStart = textarea.selectionStart; const selectEnd = textarea.selectionEnd; const len = text.length; // 从选择开始位置向前查找最近的'[' let leftBracket = null; for (let i = selectStart - 1; i >= 0; i--) { if (text[i] === '[') { leftBracket = i; break; } } // 从选择结束位置向后查找最近的']' let rightBracket = null; for (let i = selectEnd; i < len; i++) { if (text[i] === ']') { rightBracket = i; break; } } // 计算最终选择范围: const newStart = leftBracket !== null ? leftBracket : 0; // 未找到[则从开头开始 const newEnd = rightBracket !== null ? rightBracket + 1 : len; // 未找到]则到末尾结束 // 执行选择 textarea.focus(); textarea.selectionStart = newStart; textarea.selectionEnd = newEnd; } //编辑框页面优化 (function () { // 判断是否为发帖页面 const url = new URL(window.location.href); if (url.searchParams.get('mod') !== 'post') return; // 插入图片显示隐藏 const targetDiv = document.querySelector('#comiis_mh_sub>div'); if (targetDiv) { // 创建要插入的元素 const newLink = document.createElement('a'); newLink.href = 'javascript:;'; newLink.className = 'comiis_pictitle'; newLink.innerHTML = '图片0'; // 插入到div内部作为子元素(末尾) targetDiv.appendChild(newLink); // 绑定点击事件 const imgList = document.querySelector('#imglist'); const comiisPostTab = document.querySelector('#comiis_post_tab'); imgList.style.display = 'none' comiisPostTab.appendChild(imgList); newLink.addEventListener('click', function () { // // 获取自身class是否包含f_0 // const hasF0 = this.querySelector('i').classList.contains('f_0'); // if (imgList) { // imgList.style.display = hasF0 ? 'none' : 'block'; // } pictureManagementTool() }); } // 按钮移至顶部(适配多个comiis_btnbox) (function () { // 获取所有comiis_btnbox元素 const btnBoxList = document.querySelectorAll('.comiis_btnbox'); // 获取目标插入位置的headDiv const headDiv = document.querySelector('#comiis_head>div'); // 校验核心元素 if (btnBoxList.length === 0 || !headDiv) { console.warn('按钮移顶时未找到核心元素元素!'); return; } // 创建顶部父容器并设置样式 const headerY = document.createElement('div'); headerY.className = 'header_y'; Object.assign(headerY.style, { display: 'flex', alignContent: 'center', alignItems: 'center', justifyContent: 'flex-end', height: '100%' }); // 遍历所有comiis_btnbox元素处理 btnBoxList.forEach((btnBox, index) => { // 遍历当前btnBox的子元素处理 Array.from(btnBox.children).forEach(child => { if (child.nodeType !== 1) return; const bgClass = Array.from(child.classList).find(cls => cls.startsWith('bg_')); if (!bgClass) return; // 创建新元素并设置样式和内容 const btnDiv = document.createElement('div'); // 若多个btnBox的bgClass重复,可添加索引区分 btnDiv.className = `${bgClass}`; btnDiv.textContent = child.textContent.trim(); Object.assign(btnDiv.style, { padding: '5px', whiteSpace: 'nowrap', margin: '10px', }); // 绑定点击事件(触发原child的点击) btnDiv.addEventListener('click', () => { child.click(); }); // 加入父容器 headerY.appendChild(btnDiv); }); // 隐藏当前btnBox btnBox.style.display = 'none'; }); // 插入父容器到headDiv if (headerY.children.length > 0) { headDiv.appendChild(headerY); } })(); // 增加textarea的显示大小 (function () { // 禁用默认的textarea高度调整函数 function disableDefaultHeightAdjust() { window.textarea_scrollHeight = () => { }; } // 计算并设置form和textarea的高度(核心逻辑) function calculateAndSetHeight() { const postForm = document.querySelector('#postform>div'); const needMessage = document.querySelector('#needmessage'); if (!postForm || !needMessage) { console.log('计算textarea时未找到核心元素!') return; } // 2. 计算form内除textarea外的元素总高度:form高度 - textarea高度 const formExceptTextareaHeight = postForm.offsetHeight - needMessage.offsetHeight; // 3. 重新计算textarea高度:网页高度 - 其他元素总高度 - 预留间距(避免边界溢出) const padding = 50; let textareaHeight = document.documentElement.clientHeight - formExceptTextareaHeight - padding; // 减少 100px,提供底部冗余空间,避免竖直滚动条 textareaHeight = textareaHeight - 100; // 限制最小高度(避免过小) textareaHeight = Math.max(100, textareaHeight); // 取整避免小数像素渲染问题 textareaHeight = Math.floor(textareaHeight); // 4. 设置textarea高度(宽度自适应form,box-sizing确保计算准确) needMessage.style.cssText = ` height: ${textareaHeight}px; width: 100%; box-sizing: border-box; margin: 0; padding: 4px; /* 可根据实际需求调整 */ `; } // 监听窗口resize事件(防抖处理) function listenWindowResize() { let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(calculateAndSetHeight, 100); }); } // 监听#comiis_post_tab高度变化(触发form内元素高度重计算) function observeTabHeightChange() { const targetSelector = '#comiis_post_tab'; const target = document.querySelector(targetSelector); if (target) { initObserver(target); } else { // 等待元素加载(简化版) const observer = new MutationObserver((mutations, obs) => { const el = document.querySelector(targetSelector); if (el) { obs.disconnect(); initObserver(el); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化MutationObserver function initObserver(element) { let lastHeight = element.clientHeight; const observer = new MutationObserver(() => { const currentHeight = element.clientHeight; if (currentHeight !== lastHeight) { lastHeight = currentHeight; calculateAndSetHeight(); } }); observer.observe(element, { attributes: true, attributeFilter: ['style', 'class'], childList: true, subtree: true }); } } // 初始化所有逻辑 (function () { disableDefaultHeightAdjust(); // 页面加载完成后执行初始计算 calculateAndSetHeight(); listenWindowResize(); observeTabHeightChange(); })() })() })() })();