// ==UserScript== // @name 保留文本替换-手机优化版 7.3.9(最终版) // @namespace https://viayoo.com/ // @version 7.3.9 // @description 7.3.9 最终定稿:修复 Base64 跨域、打印空白页、公众号漏替换、规则统计实时刷新;支持文本替换 / 查找替换 / 规则管理 / 文章保存 / 悬浮图片,专为移动端 & WebView 环境裁剪优化,无破坏性变更,可长期稳定使用。 // @author You // @run-at document-end // @match https:/// // @match :///* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function() { ‘use strict’; // 脚本自身UI字体(悬浮球/菜单/弹窗,固定14px,不影响网页) let size = 14; // 1. 抓取网页原生真实字号 const originPageFont = parseInt(getComputedStyle(document.body).fontSize) || 16; // 2. 网页可调节字号:有本地记录用记录,无则用原生字号 // 读取本地保存的字号(带px) // 读取本地纯数字,无数据默认给一个合理值,避免 NaN let textSize = parseInt(localStorage.getItem(‘textSize’)) || 16; const SIZE_STEP = 1; const MIN_SIZE = 1; const MAX_SIZE = 120; const LONG_DELAY = 250; const REPEAT_INTERVAL = 80; // 字体调节开关:默认关闭 let fontAdjustSwitch = GM_getValue(‘fontAdjustSwitch’, false); // 全局锁定脚本面板字体(原有样式保留) const fixedFontStyle = document.createElement(‘style’); fixedFontStyle.id = ‘tm-fixed-ui-font’; fixedFontStyle.textContent = ` /* 最高权重:保护所有 tm- 开头组件,覆盖三方全局规则 */ html body div[class^=”tm-“], html body div[class^=”tm-“] *, html body span[class^=”tm-“], html body span[class^=”tm-“] * { font-size: ${size}px !important; } /* 专项对抗:三方 span[style=”font-size:11px”] 全局匹配 */ html body .tm-floating-btn span[style=”font-size:11px”], html body .tm-mobile-menu span[style=”font-size:11px”], html body .tm-highlight span[style=”font-size:11px”], html body .tm-find-highlight span[style*=”font-size:11px”] { font-size: ${size}px !important; } /* 专项对抗:三方 fixed / top+left 定位选择器 / html body .tm-floating-btn[style=”position:fixed”], html body .tm-floating-btn[style=”position:fixed”] *, html body .tm-mobile-menu[style=”top:”][style=”left:”], html body .tm-mobile-menu[style=”top:”][style*=”left:”] * { font-size: ${size}px !important; } /* 原有自身样式保留 */ html body .tm-floating-btn, html body .tm-floating-btn *, html body .tm-mobile-menu, html body .tm-mobile-menu *, html body .tm-find-overlay, html body .tm-find-overlay *, html body .tm-rule-manager, html body .tm-rule-manager *, html body .tm-replace-dialog-mask, html body .tm-replace-dialog-mask *, html body .tm-toast, html body .tm-toast *, html body .tm-highlight, html body .tm-highlight *, html body .tm-find-highlight, html body .tm-find-highlight *, html body .img-set-panel, html body .img-set-panel * { font-size: ${size}px !important; } html body .tm-mobile-menu [style*=”font-size”] { font-size: ${size}px !important; } `; document.head.appendChild(fixedFontStyle); // ========== 下面保留你原本所有:状态变量、函数、创建悬浮球代码 ========== // (这里原样不动,继续写你原来的代码) // 状态变量 let isReplacing = false; let replaceRules = GM_getValue(‘saveReplaceRules’, []); let isEnabled = GM_getValue(‘enabled’, true); let highlightEnabled = GM_getValue(‘highlightEnabled’, false); let highlightDuration = GM_getValue(‘highlightDuration’, 5); let observer = null; let originalTextMap = new Map(); let highlightTimeoutMap = new Map(); let totalReplacedCount = 0; let isPrinting = false; const INLINE_TAGS = [‘SPAN’,’STRONG’,’EM’,’B’,’I’,’U’,’A’,’FONT’]; const INLINE_NODE_TAGS = [‘SPAN’, ‘EM’, ‘STRONG’, ‘B’, ‘I’, ‘U’, ‘FONT’]; // 规则计数全局变量 let activeRuleSet = new Set(); let enabledRuleCount = 0; // 查找功能全局变量 let findFuzzyEnable = GM_getValue(‘findFuzzyEnable’, true); let findOverlay = null; let findHighlights = []; let currentFindIndex = -1; let replaceHistoryStack = []; let replaceAllBackup = []; let debounceTimer = null; let tapTimer = null; let longPressTimer = null; let isDragging = false; let isLongPress = false; let lastTapTime = 0; let isAllLongPress = false; let allPressTimer = null; // 标记自身面板class前缀,禁止替换 const SELF_PANEL_CLASS = [ ‘tm-floating-btn’, ‘tm-toast’, ‘tm-mobile-menu’, ‘tm-rule-manager’, ‘tm-replace-dialog-mask’, ‘tm-replace-dialog-box’, ‘tm-highlight’, ‘tm-find-overlay’, ‘tm-find-highlight’ ]; function isSelfPanelNode(node){ if(!node) return false; let cur = node; while(cur){ if(cur.className && typeof cur.className === 'string'){ const cls = cur.className; for(let c of SELF_PANEL_CLASS){ if(cls.includes(c)) return true; } } cur = cur.parentElement; } return false; } // 防抖函数 function debounce(func, wait = 20) { return function executedFunction(...args) { const later = () => { clearTimeout(debounceTimer); func(...args); }; clearTimeout(debounceTimer); debounceTimer = setTimeout(later, wait); }; } // 初始化监听器 function initObserver() { if (observer) { observer.disconnect(); observer.takeRecords(); } observer = new MutationObserver(debounce(mutations => { if (!isReplacing || !isEnabled) return; let processed = 0; for (const mutation of mutations) { if (processed > 2000) break; if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === 1 && !isSelfPanelNode(node)) { scan(node); processed++; } } } else if (mutation.type === 'characterData') { if(!isSelfPanelNode(mutation.target)) applyReplace(mutation.target); processed++; } } })); } // 判断正则格式 function isRegexRule(str) { return typeof str === 'string' && str.startsWith('/') && str.lastIndexOf('/') > 0; } // 安全解析正则 function getRegex(str) { try { const last = str.lastIndexOf('/'); const regStr = str.slice(1, last); const flag = str.slice(last + 1) || 'g'; if (regStr.length > 200) return null; return new RegExp(regStr, flag); } catch (e) { console.warn('正则解析失败:', str); return null; } } // 统计所有已启用的规则总数 function calcEnabledRuleTotal() { let count = 0; replaceRules.forEach(rule => { if (rule.enabled && rule.from && rule.from.trim() !== '') { count++; } }); enabledRuleCount = count; return count; } // 重置生效规则集合 function resetActiveRuleSet() { activeRuleSet.clear(); } // 保存原文 function saveOriginalText(node, text) { originalTextMap.set(node, text); } // 获取原文 function getOriginalText(node) { return originalTextMap.get(node) || node.textContent; } // 高亮显示 function highlightReplacedText(node, newText) { if (!highlightEnabled || !node.parentNode) return node; if (node.nodeType === 1 && node.classList?.contains('tm-highlight')) return node; if (node.parentNode?.classList?.contains('tm-highlight')) return node.parentNode; const span = document.createElement('span'); span.className = 'tm-highlight'; span.style.cssText = ` background-color: rgba(255, 235, 59, 0.3); border-radius: 2px; padding: 1px 2px; transition: background-color 0.3s; display: inline; `; if (node.nodeType === Node.TEXT_NODE) { span.textContent = newText || node.textContent; node.parentNode.replaceChild(span, node); } else { const clone = node.cloneNode(true); clone.textContent = newText || node.textContent; span.appendChild(clone); node.parentNode.replaceChild(span, node); } const durSecond = GM_getValue('highlightDuration', 5); const durMs = durSecond * 1000; if (durMs > 0) { const timeoutId = setTimeout(() => removeHighlight(span), durMs); highlightTimeoutMap.set(span, timeoutId); } return span; } // 移除高亮 function removeHighlight(span) { if (!span || !span.parentNode) return; if (highlightTimeoutMap.has(span)) { clearTimeout(highlightTimeoutMap.get(span)); highlightTimeoutMap.delete(span); } const textNode = document.createTextNode(span.textContent); span.parentNode.replaceChild(textNode, span); } // 清空全部高亮 function clearAllHighlights() { document.querySelectorAll('.tm-highlight').forEach(span => removeHighlight(span)); highlightTimeoutMap.clear(); } // 文本替换核心 function applyReplace(node) { if(isSelfPanelNode(node)) return; if (node.nodeType !== Node.TEXT_NODE || !isReplacing || !isEnabled) return; if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode?.tagName)) return; if (node.parentNode?.classList?.contains('tm-highlight')) return; let originalText = node.textContent; let text = originalText; let changed = false; replaceRules.forEach((rule, ruleIdx) => { if (!rule.enabled || !rule.from) return; if (isRegexRule(rule.from)) { const regex = getRegex(rule.from); if (regex) { try { const newText = text.replace(regex, rule.to); if (newText !== text) { text = newText; changed = true; activeRuleSet.add(ruleIdx); } } catch (e) {} } } else { if (text.includes(rule.from)) { text = text.split(rule.from).join(rule.to); changed = true; activeRuleSet.add(ruleIdx); } } }); if (changed && !originalTextMap.has(node)) { saveOriginalText(node, originalText); totalReplacedCount++; highlightEnabled ? highlightReplacedText(node, text) : node.textContent = text; } } // 收集被行内标签拆分的连续文本节点组 function getInlineTextGroups(root) { const groups = []; let currentGroup = []; function traverse(el) { if (isSelfPanelNode(el)) return; if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return; if (el.nodeType === Node.TEXT_NODE) { if (el.textContent.trim() === '') return; currentGroup.push(el); return; } if (!INLINE_NODE_TAGS.includes(el.tagName)) { if (currentGroup.length > 0) { groups.push([...currentGroup]); currentGroup = []; } } for (const child of el.childNodes) { traverse(child); } } traverse(root); if (currentGroup.length > 0) { groups.push([...currentGroup]); } return groups; } // 扫描页面文本【修复公众号漏替换】 function scan(element) { if(isSelfPanelNode(element)) return; if (!element || !isReplacing || !isEnabled) return; const textGroups = getInlineTextGroups(element); textGroups.forEach(group => { const hasProcessed = group.some(node => originalTextMap.has(node)); if (hasProcessed) return; let fullText = group.map(n => n.textContent).join(''); const originText = fullText; let totalMatch = 0; replaceRules.forEach((rule, ruleIdx) => { if (!rule.enabled || !rule.from) return; if (isRegexRule(rule.from)) { const reg = getRegex(rule.from); if (!reg) return; try { const matchArr = originText.match(reg); if (matchArr) { totalMatch += matchArr.length; activeRuleSet.add(ruleIdx); } fullText = fullText.replace(reg, rule.to); } catch (e) {} } else { const cnt = (originText.split(rule.from).length - 1); if (cnt > 0) { totalMatch += cnt; activeRuleSet.add(ruleIdx); } fullText = fullText.split(rule.from).join(rule.to); } }); if (totalMatch > 0) { totalReplacedCount += totalMatch; let ptr = 0; group.forEach(node => { const oldText = node.textContent; const len = oldText.length; const newText = fullText.slice(ptr, ptr + len); ptr += len; originalTextMap.set(node, oldText); if (highlightEnabled) { highlightReplacedText(node, newText); } else { node.textContent = newText; } }); } }); // 兜底独立文本节点 const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if(isSelfPanelNode(node)) return NodeFilter.FILTER_REJECT; if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode?.tagName)) { return NodeFilter.FILTER_REJECT; } if (originalTextMap.has(node)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }, false ); let nodeCount = 0; while (walker.nextNode() && nodeCount < 99999) { applyReplace(walker.currentNode); nodeCount++; } } // 启停替换 function startReplace() { resetActiveRuleSet(); calcEnabledRuleTotal(); if (isReplacing) { showToast("替换已在运行", "info"); return; } isReplacing = true; if (document.body) { scan(document.body); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } updateFloatingButton(); showToast("替换已开启", "success"); } function stopReplace() { if (!isReplacing) { showToast("替换已停止", "info"); return; } isReplacing = false; if (observer) observer.disconnect(); clearAllHighlights(); if (document.body) { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if(isSelfPanelNode(node)) return NodeFilter.FILTER_REJECT; if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode?.tagName)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false ); while (walker.nextNode()) { const node = walker.currentNode; const original = getOriginalText(node); if (original && node.textContent !== original) { node.textContent = original; } } } totalReplacedCount = 0; originalTextMap.clear(); resetActiveRuleSet(); updateFloatingButton(); showToast("替换已停止,恢复原文", "info"); } // 重新应用全部规则(修复版:无报错) function forceReapplyAllRules() { restoreOriginalKeepRules(); calcEnabledRuleTotal(); if (isReplacing && document.body) { scan(document.body); } showToast("已重载全部替换规则", 'success'); } function toggleReplace() { isReplacing ? stopReplace() : startReplace(); updateFloatingButton(); calcEnabledRuleTotal(); //1. 顶部开始/停止按钮实时改字 const menuBtn = document.querySelector('.main-switch-btn'); if (menuBtn) { const stat = isReplacing; menuBtn.innerHTML = `<span style="font-size:15px;margin-right:2px;">${stat?'⏸️':'▶️'}</span><span style="color:${stat?'red':'green'};">${stat?'停止替换':'开始替换'}</span>`; } //2. 菜单里规则管理那行统计文字刷新 const ruleMenuText = document.querySelector('[dynamicText="true"]'); if(ruleMenuText){ const openCnt = calcEnabledRuleTotal(); const total = replaceRules.length; const closeCnt = total - openCnt; ruleMenuText.textContent = `🔧 规则管理 | 开启:${openCnt}条 | 关闭:${closeCnt}条 | 总计:${total}条 | 本页生效:${activeRuleSet.size}条`; } } // 高亮开关 function toggleHighlight() { highlightEnabled = !highlightEnabled; GM_setValue(‘highlightEnabled’, highlightEnabled); if (!highlightEnabled) { clearAllHighlights(); } if (isEnabled && isReplacing && observer) { scan(document.body); } } // 恢复原文 保留规则【修复:不再stopReplace打断运行】 function restoreOriginalKeepRules() { if (!document.body) return; // 只恢复原文,不关闭替换开关 const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if(isSelfPanelNode(node)) return NodeFilter.FILTER_REJECT; if ([‘SCRIPT’, ‘STYLE’, ‘NOSCRIPT’].includes(node.parentNode?.tagName)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false ); while (walker.nextNode()) { const node = walker.currentNode; const original = getOriginalText(node); if (original && node.textContent !== original) { node.textContent = original; } } totalReplacedCount = 0; originalTextMap.clear(); clearAllHighlights(); resetActiveRuleSet(); showToast(“已恢复原文,规则已保存”, “success”); } // 恢复原文 + 清空所有规则 function restoreOriginalClearRules() { if (!document.body) return; const wasReplacing = isReplacing; if (wasReplacing) stopReplace(); replaceRules = []; GM_setValue(‘saveReplaceRules’, []); originalTextMap.clear(); clearAllHighlights(); location.reload(); } // 纯字体大小调节(仅改 font-size,不缩放布局,悬浮球完全不受影响) function setTextSize(size) { size = Math.max(MIN_SIZE, Math.min(MAX_SIZE, size)); textSize = size; localStorage.setItem(‘textSize’, textSize); let oldCss = document.getElementById('text-size-style'); if (oldCss) oldCss.remove(); const style = document.createElement('style'); style.id = 'text-size-style'; style.textContent = ` body, p, div, span, a, li, td, th, h1, h2, h3, h4, h5, h6, em, strong, b, i, u, label, input, textarea { font-size: ${size}px !important; } @media print { .tm-floating-btn, .no-print { display: none !important; } } `; document.head.appendChild(style); showToast(`当前字体:${size}px`, "info"); } // 缩小字体 function fontShrink() { setTextSize(textSize - SIZE_STEP); } // 放大字体 function fontEnlarge() { setTextSize(textSize + SIZE_STEP); } // 重置字体 function fontReset() { setTextSize(16); } // ========== 页面查找功能 ========== function closeFindOverlay() { if (findOverlay) { findOverlay.remove(); findOverlay = null; } findHighlights.forEach(span => { if (span && span.parentNode) { span.parentNode.replaceChild(document.createTextNode(span.textContent), span); } }); findHighlights = []; currentFindIndex = -1; } function getAllTextNodes(root) { let nodes = []; const walk = (el) => { if (isSelfPanelNode(el)) return; if (el.nodeType === Node.TEXT_NODE) { if (!['SCRIPT','STYLE','NOSCRIPT','IFRAME'].includes(el.parentNode?.tagName)) { nodes.push(el); } return; } for (let child of el.childNodes) walk(child); }; walk(root); return nodes; } // 最小化为圆点 function minimizePanel() { if (!findOverlay) return; if (findOverlay.isPanelMini) return; findOverlay.isPanelMini = true; findOverlay.oldPanelStyle = { left: findOverlay.style.left, top: findOverlay.style.top, transform: findOverlay.style.transform, width: findOverlay.style.width, maxWidth: findOverlay.style.maxWidth, borderRadius: findOverlay.style.borderRadius, height: findOverlay.style.height }; findOverlay.style.width = '40px'; findOverlay.style.height = '40px'; findOverlay.style.borderRadius = '50%'; findOverlay.style.left = 'calc(100% - 50px)'; findOverlay.style.top = 'calc(100% - 50px)'; findOverlay.style.transform = 'none'; Array.from(findOverlay.children).forEach(el => { el.style.visibility = 'hidden'; el.style.opacity = '0'; }); } // 还原正常面板 function restorePanel() { if (!findOverlay || !findOverlay.isPanelMini) return; findOverlay.isPanelMini = false; const st = findOverlay.oldPanelStyle; findOverlay.style.left = st.left; findOverlay.style.top = st.top; findOverlay.style.transform = st.transform; findOverlay.style.width = st.width; findOverlay.style.maxWidth = st.maxWidth; findOverlay.style.borderRadius = st.borderRadius; findOverlay.style.height = st.height; Array.from(findOverlay.children).forEach(el => { el.style.visibility = ''; el.style.opacity = ''; }); const contentWrap = findOverlay.querySelectorAll('div')[2]; const replaceRow = findOverlay.querySelectorAll('div')[3]; if (contentWrap) { contentWrap.style.flexWrap = 'nowrap'; contentWrap.style.overflowX = 'auto'; } if (replaceRow) { replaceRow.style.flexWrap = 'nowrap'; replaceRow.style.overflowX = 'auto'; } } function doFindInPage(keyword) { findHighlights.forEach(span => { if (span && span.parentNode) { span.parentNode.replaceChild(document.createTextNode(span.textContent), span); } }); findHighlights = []; currentFindIndex = -1; if (!keyword.trim()) { updateFindCount(0, 0); showToast("请输入查找内容", "warning"); return; } let reg; if (keyword.startsWith('/') && keyword.lastIndexOf('/') > 0) { const lastSlash = keyword.lastIndexOf('/'); const pattern = keyword.slice(1, lastSlash); const flags = keyword.slice(lastSlash + 1) || 'gi'; try { reg = new RegExp(pattern, flags); } catch (e) { showToast("正则格式错误", "error"); return; } } else { const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if(findFuzzyEnable){ reg = new RegExp(escaped.split('').join('.*?'), 'gi'); }else{ reg = new RegExp(escaped, 'gi'); } } const textGroups = getInlineTextGroups(document.body); textGroups.forEach(group => { let fullText = group.map(n => n.textContent).join(''); let matches = []; let res; reg.lastIndex = 0; while ((res = reg.exec(fullText)) !== null) { matches.push({start: res.index, end: reg.lastIndex}); } if(matches.length===0) return; let ptr = 0; group.forEach(node => { const oldText = node.textContent; const len = oldText.length; const nodeStart = ptr; const nodeEnd = ptr + len; ptr += len; let inNodeMatches = matches.filter(m=>m.start < nodeEnd && m.end > nodeStart); if(inNodeMatches.length===0) return; let text = node.textContent; let frag = document.createDocumentFragment(); let last = 0; inNodeMatches.forEach(m=>{ const s = Math.max(m.start - nodeStart, 0); const e = Math.min(m.end - nodeStart, len); if(s>last) frag.append(document.createTextNode(text.slice(last,s))); const hitSpan = document.createElement('span'); hitSpan.className = 'tm-find-highlight'; hitSpan.style.cssText = 'background:#ffeb3b;color:#000;border-radius:2px;padding:0 2px;'; hitSpan.textContent = text.slice(s,e); frag.append(hitSpan); findHighlights.push(hitSpan); last = e; }); if(last < len) frag.append(document.createTextNode(text.slice(last))); node.parentNode.replaceChild(frag, node); }); }); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if(isSelfPanelNode(node)) return NodeFilter.FILTER_REJECT; if (['SCRIPT','STYLE','NOSCRIPT'].includes(node.parentNode?.tagName)) return NodeFilter.FILTER_REJECT; if (node.parentNode.classList?.contains('tm-find-highlight')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }, false ); let node; while(node = walker.nextNode()){ let text = node.textContent; let matches = []; let res; reg.lastIndex=0; while ((res = reg.exec(text)) !== null) { matches.push({start: res.index, end: reg.lastIndex}); } for (let i = matches.length - 1; i >= 0; i--) { const m = matches[i]; const preStr = text.substring(0, m.start); const hitStr = text.substring(m.start, m.end); const sufStr = text.substring(m.end); const highlightSpan = document.createElement('span'); highlightSpan.className = 'tm-find-highlight'; highlightSpan.style.cssText = 'background:#ffeb3b;color:#000;border-radius:2px;padding:0 2px;'; highlightSpan.textContent = hitStr; const fragment = document.createDocumentFragment(); if (preStr) fragment.append(document.createTextNode(preStr)); fragment.append(highlightSpan); if (sufStr) fragment.append(document.createTextNode(sufStr)); node.parentNode.replaceChild(fragment, node); findHighlights.push(highlightSpan); node = fragment.firstChild; } } const totalNum = findHighlights.length; updateFindCount(currentFindIndex+1, totalNum); if (totalNum === 0) { showToast(`未找到:${keyword}`, "info"); return; } currentFindIndex = 0; jumpToFind(0); showToast(`共找到 ${totalNum} 处`, "success"); } function doReplaceCurrent(findTxt, replaceTxt) { if (!findTxt.trim()) return showToast("请输入查找内容", "warning"); if (findHighlights.length === 0 || currentFindIndex < 0) { return showToast("暂无可替换内容", "warning"); } const escaped = findTxt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const reg = new RegExp(escaped, 'gi'); const span = findHighlights[currentFindIndex]; replaceHistoryStack.push({ index: currentFindIndex, text: span.textContent }); const oldText = span.textContent; const newText = oldText.replace(reg, replaceTxt); if (oldText !== newText) { span.textContent = newText; showToast("替换完成,可继续替换或撤销", "success"); } jumpToFind(1); } function doReplaceAll(findTxt, replaceTxt) { let searchVal = findTxt.trim(); if (!searchVal) return showToast("请输入查找内容", "warning"); let highlights = document.querySelectorAll(".tm-find-highlight"); if (highlights.length <= 0) return showToast("暂无匹配内容", "warning"); let count = 0; replaceAllBackup = []; highlights.forEach(el => { let oldTxt = el.textContent; replaceAllBackup.push({node: el, old: oldTxt}); let newTxt = oldTxt.split(searchVal).join(replaceTxt); if (oldTxt !== newTxt) { el.textContent = newTxt; count++; } }); totalReplacedCount = count; showToast(`替换成功,共${count}处`, "success"); } function undoReplaceAll() { if (replaceAllBackup.length === 0) return showToast("无内容可还原", "info"); replaceAllBackup.forEach(item => { if(item.node) item.node.textContent = item.old; }); replaceAllBackup = []; showToast("已还原全部内容", "success"); } function jumpToFind(step) { const total = findHighlights.length; if (total === 0) return; currentFindIndex += step; if (currentFindIndex >= total) currentFindIndex = 0; else if (currentFindIndex < 0) currentFindIndex = total - 1; findHighlights.forEach((sp, i) => { sp.style.background = i === currentFindIndex ? '#ffc107' : '#ffeb3b'; }); findHighlights[currentFindIndex].scrollIntoView({ behavior: "smooth", block: "center" }); updateFindCount(currentFindIndex + 1, total); } function updateFindCount(cur, total) { if (!findOverlay) return; const countDom = findOverlay.querySelector('.tm-find-count'); if (countDom) { countDom.innerText = `${cur}/${total}`; countDom.style.display = total > 0 ? 'inline-block' : 'none'; } } function undoReplace() { if (replaceHistoryStack.length === 0) { showToast("没有可撤销的内容", "info"); return; } const lastRecord = replaceHistoryStack.pop(); const targetIndex = lastRecord.index; const originalText = lastRecord.text; const targetSpan = findHighlights[targetIndex]; if (!targetSpan) { showToast("无法定位到被替换的元素", "error"); return; } targetSpan.textContent = originalText; showToast("已撤销上一次替换", "success"); currentFindIndex = targetIndex; findHighlights.forEach((sp, i) => { sp.style.background = i === currentFindIndex ? '#ffc107' : '#ffeb3b'; }); findHighlights[currentFindIndex].scrollIntoView({ behavior: "smooth", block: "center" }); updateFindCount(currentFindIndex + 1, findHighlights.length); } function showFindOverlay() { if (findOverlay) return; const initialLeft = (window.innerWidth - 380) / 2; const initialTop = Math.max(15, window.innerHeight * 0.12); const overlay = document.createElement('div'); overlay.className = 'tm-find-overlay'; overlay.style.cssText = ` position: fixed; left: 50%; top:${initialTop}px; transform: translateX(-50%); width:96%; max-width:380px; background:rgba(255,255,255,0.50); backdrop-filter:blur(12px); border-radius:12px; box-shadow:0 4px 16px rgba(0,0,0,0.15); z-index:2147483647; display:flex; flex-direction:column; overflow:hidden; box-sizing:border-box !important; transition:all 0.2s ease; `; overlay.addEventListener('click', function(e) { const target = e.target; if(target.closest('.mini-btn') || target.closest('button')) return; if (this.isPanelMini) { e.stopPropagation(); restorePanel(); } }); const tabBar = document.createElement('div'); tabBar.style.cssText = `display:flex;border-bottom:1px solid #eee;`; const tabFind = document.createElement('div'); tabFind.textContent = '查找'; tabFind.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid #007aff;color:#007aff;cursor:pointer;`; const tabReplace = document.createElement('div'); tabReplace.textContent = '替换'; tabReplace.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid transparent;color:#333;cursor:pointer;`; let isReplaceMode = false; tabFind.onclick = () => { isReplaceMode = false; tabFind.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid #007aff;color:#007aff;cursor:pointer;`; tabReplace.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid transparent;color:#333;cursor:pointer;`; replaceRow.style.display = 'none'; }; tabReplace.onclick = () => { isReplaceMode = true; tabReplace.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid #007aff;color:#007aff;cursor:pointer;`; tabFind.style.cssText = `flex:1;padding:10px 0;text-align:center;font-size:14px;border-bottom:2px solid transparent;color:#333;cursor:pointer;`; replaceRow.style.display = 'flex'; }; tabBar.append(tabFind, tabReplace); const dragBar = document.createElement('div'); dragBar.className = "dragBar"; dragBar.style.cssText = `height:22px;background:#f5f7fa;cursor:move;display:flex;align-items:center;justify-content:space-between;padding:0 10px;`; dragBar.innerHTML = ` <div style="width:32px;height:3px;background:#ccc;border-radius:3px;"></div> <span class="mini-btn" style="font-size:18px;color:#666;cursor:pointer;user-select:none;">−</span> `; dragBar.querySelector('.mini-btn').onclick = function (e) { e.stopPropagation(); minimizePanel(); }; const contentWrap = document.createElement('div'); contentWrap.style.cssText = `display:flex;align-items:center;gap:6px;padding:8px 10px;flex-wrap:nowrap !important;overflow-x:auto;width:100%;box-sizing:border-box;`; const inputWrap = document.createElement('div'); inputWrap.style.cssText = `flex:1 1 180px;position:relative;min-width:80px;transition:flex 0.2s ease;`; const input = document.createElement('input'); input.id = "searchInput"; input.type = 'text'; input.placeholder = '查找内容'; input.style.cssText = `width:100%;padding:8px 28px 8px 10px;border:1px solid rgba(226,229,233,0.5);border-radius:8px;font-size:13px;background:rgba(255,255,255,0.4);backdrop-filter:blur(10px);box-sizing:border-box;transition: border-color 0.2s;`; input.addEventListener('focus', function(){ this.style.borderColor = '#409eff'; }); input.addEventListener('blur', function(){ this.style.borderColor = '#e2e5e9'; }); const clearBtn = document.createElement('span'); clearBtn.innerText = '×'; clearBtn.style.cssText = `position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:16px;color:#999;cursor:pointer;user-select:none;display:none;`; clearBtn.onclick = () => { input.value = ''; clearBtn.style.display = 'none'; countSpan.style.display = 'none'; inputWrap.style.flex = '1 1 180px'; updateFindCount(0,0); input.focus(); }; inputWrap.appendChild(input); inputWrap.appendChild(clearBtn); const countSpan = document.createElement('span'); countSpan.className = 'tm-find-count'; countSpan.innerText = '0/0'; countSpan.style.cssText = `font-size:13px;color:#555;min-width:40px;text-align:center;flex-shrink:0;display:none;`; const makeBtn = (txt, bg, color) => { const btn = document.createElement('button'); btn.innerText = txt; btn.style.cssText = `padding:5px 7px;border:none;border-radius:6px;background:${bg};color:${color};font-size:12px;white-space:nowrap;flex-shrink:0;`; return btn; }; const btnSearch = makeBtn('搜', '#3b82f6', '#fff'); const btnPrev = makeBtn('⤴', '#f1f5f9', '#333'); const btnNext = makeBtn('⤵', '#f1f5f9', '#333'); const btnClose = makeBtn('✕', '#ef4444', '#fff'); btnSearch.onclick = () => { const kw = input.value.trim(); doFindInPage(kw); if(kw){ countSpan.style.display = 'inline-block'; inputWrap.style.flex = '1 1 120px'; } }; input.onkeydown = e => { if (e.key === 'Enter') { const kw = input.value.trim(); doFindInPage(kw); if(kw){ countSpan.style.display = 'inline-block'; inputWrap.style.flex = '1 1 120px'; } } }; btnPrev.onclick = () => jumpToFind(-1); btnNext.onclick = () => jumpToFind(1); btnClose.onclick = closeFindOverlay; const fuzzySwitch = makeBtn(findFuzzyEnable ? '模糊🔛' : '精准🔚', findFuzzyEnable ? '#9366ff' : '#9ca3af', '#fff'); fuzzySwitch.onclick = () => { findFuzzyEnable = !findFuzzyEnable; GM_setValue('findFuzzyEnable', findFuzzyEnable); fuzzySwitch.innerText = findFuzzyEnable ? '模糊🔛' : '精准🔚'; fuzzySwitch.style.background = findFuzzyEnable ? '#9366ff' : '#9ca3af'; }; contentWrap.append(inputWrap, countSpan, fuzzySwitch, btnSearch, btnPrev, btnNext, btnClose); const replaceRow = document.createElement('div'); replaceRow.style.cssText = `display:none;align-items:center;gap:6px;padding:0 10px 8px;flex-wrap:nowrap !important;overflow-x:auto;width:100%;box-sizing:border-box;`; const replaceInputWrap = document.createElement('div'); replaceInputWrap.style.cssText = `flex:1;position:relative;`; const replaceInput = document.createElement('input'); replaceInput.id = "replaceInput"; replaceInput.type = 'text'; replaceInput.placeholder = '替换为'; replaceInput.style.cssText = `width:100%;padding:8px 28px 8px 10px;border:1px solid rgba(226,229,233,0.5);border-radius:8px;font-size:13px;background:rgba(255,255,255,0.4);backdrop-filter:blur(10px);box-sizing:border-box;transition: border-color 0.2s;`; replaceInput.addEventListener('focus', function(){ this.style.borderColor = '#9366ff'; }); replaceInput.addEventListener('blur', function(){ this.style.borderColor = '#e2e5e9'; }); const replaceClearBtn = document.createElement('span'); replaceClearBtn.innerText = '×'; replaceClearBtn.style.cssText = `position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:16px;color:#999;cursor:pointer;user-select:none;display:none;`; replaceInput.oninput = function(){ replaceClearBtn.style.display = this.value.trim() ? 'block' : 'none'; }; replaceClearBtn.onclick = function(){ replaceInput.value = ''; replaceClearBtn.style.display = 'none'; replaceInput.focus(); }; replaceInputWrap.appendChild(replaceInput); replaceInputWrap.appendChild(replaceClearBtn); const btnSwap = document.createElement('button'); btnSwap.style.cssText = ` padding:5px 7px; border:none; border-radius:6px; background: transparent; color:#8b5cf6; font-size:12px; white-space:nowrap; flex-shrink:0; display:flex; align-items:center; justify-content:center; cursor:pointer; `; btnSwap.innerHTML = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"> <path d="M23 4v6h-6"></path> <path d="M1 20v-6h6"></path> <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> </svg> `; btnSwap.onclick = swapInputText; const btnUndo = makeBtn('撤销', '#f59e0b', '#fff'); btnUndo.onclick = undoReplace; const btnReplaceCurrent = makeBtn('替换', '#007aff', '#fff'); btnReplaceCurrent.onclick = ()=>{ let findTxt = input.value.trim(); let replaceTxt = replaceInput.value.trim(); doReplaceCurrent(findTxt, replaceTxt); if(!findTxt) return; const exist = replaceRules.some(item => item.from === findTxt && item.to === replaceTxt); if(!exist){ replaceRules.push({ from: findTxt, to: replaceTxt, enabled: true }); GM_setValue('saveReplaceRules', replaceRules); showToast("已自动添加为本条规则", "success"); } }; const btnReplaceAll = makeBtn('全部', '#10b981', '#fff'); let pressTimer = null; let isLongTrigger = false; const LONG_TIME = 500; btnReplaceAll.addEventListener('touchstart', e => { e.preventDefault(); isLongTrigger = false; pressTimer = setTimeout(() => { isLongTrigger = true; undoReplaceAll(); }, LONG_TIME); }); btnReplaceAll.addEventListener('touchend', e => { e.preventDefault(); clearTimeout(pressTimer); if (!isLongTrigger) { let fTxt = input.value.trim(); let rTxt = replaceInput.value.trim(); doReplaceAll(fTxt, rTxt); if(!fTxt) return; let hasRule = replaceRules.some(v=>v.from===fTxt&&v.to===rTxt); if(!hasRule){ replaceRules.push({from:fTxt,to:rTxt,enabled:true}); GM_setValue('saveReplaceRules',replaceRules); showToast("已自动添加为本条规则","success"); } } }); btnReplaceAll.addEventListener('mousedown', () => { isLongTrigger = false; pressTimer = setTimeout(() => { isLongTrigger = true; undoReplaceAll(); }, LONG_TIME); }); btnReplaceAll.addEventListener('mouseup', () => { clearTimeout(pressTimer); if (!isLongTrigger) { let fTxt = input.value.trim(); let rTxt = replaceInput.value.trim(); doReplaceAll(fTxt, rTxt); if(!fTxt) return; let hasRule = replaceRules.some(v=>v.from===fTxt&&v.to===rTxt); if(!hasRule){ replaceRules.push({from:fTxt,to:rTxt,enabled:true}); GM_setValue('saveReplaceRules',replaceRules); showToast("已自动添加为本条规则","success"); } } }); btnReplaceAll.addEventListener('mouseleave', ()=>{ clearTimeout(pressTimer); }); replaceRow.append(replaceInputWrap, btnSwap, btnUndo, btnReplaceCurrent, btnReplaceAll); overlay.append(tabBar, dragBar, contentWrap, replaceRow); document.body.appendChild(overlay); findOverlay = overlay; let dragStartX, dragStartY, startLeft, startTop, dragging = false; const startDrag = (e) => { dragging = true; const t = e.touches ? e.touches[0] : e; dragStartX = t.clientX;dragStartY = t.clientY; startLeft = overlay.offsetLeft; startTop = overlay.offsetTop; }; const moveDrag = (e) => { if (!dragging) return; const t = e.touches ? e.touches[0] : e; let dx = t.clientX - dragStartX; let dy = t.clientY - dragStartY; let newLeft = startLeft + dx; let newTop = startTop + dy; const panelW = overlay.offsetWidth; const panelH = overlay.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panelW)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - panelH)); overlay.style.left = newLeft + 'px'; overlay.style.top = newTop + 'px'; overlay.style.transform = 'none'; }; const endDrag = () => dragging = false; dragBar.addEventListener('mousedown', startDrag); dragBar.addEventListener('touchstart', startDrag); document.addEventListener('mousemove', moveDrag); document.addEventListener('touchmove', moveDrag); document.addEventListener('mouseup', endDrag); document.addEventListener('touchend', endDrag); setTimeout(() => input.focus(), 50); } function swapInputText(){ const searchInp = document.getElementById("searchInput"); const replaceInp = document.getElementById("replaceInput"); if(!searchInp || !replaceInp) return; let temp = searchInp.value; searchInp.value = replaceInp.value; replaceInp.value = temp; } // 保存当前文章 function saveCurrentArticle() { const mask = document.createElement("div"); mask.style.cssText = `position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:2147483650;display:flex;align-items:center;justify-content:center;padding:15px;`; const box = document.createElement("div"); box.style.cssText = `width:90%;max-width:360px;background:#fff;border-radius:12px;overflow:hidden;`; const titleBar = document.createElement("div"); titleBar.style.cssText = `padding:14px;text-align:center;font-size:15px;font-weight:bold;color:#222;border-bottom:1px solid #eee;`; titleBar.textContent = "选择保存方式"; const btnWrap1 = document.createElement("div"); btnWrap1.style.cssText = `padding:12px 12px 6px;display:flex;gap:8px;`; const btnWrap2 = document.createElement("div"); btnWrap2.style.cssText = `padding:6px 12px 12px;display:flex;gap:8px;`; const btnStyle = `flex:1;padding:11px;border:none;border-radius:8px;color:#fff;font-size:13px;`; const downloadTxtBtn = document.createElement("button"); downloadTxtBtn.style.cssText = btnStyle + "background:#3b82f6;"; downloadTxtBtn.textContent = "纯文本文件"; const copyTextBtn = document.createElement("button"); copyTextBtn.style.cssText = btnStyle + "background:#10b981;"; copyTextBtn.textContent = "复制全文"; // ==========改动开始:原来saveHtml按钮换成两个HTML按钮,删掉原saveHtmlBtn========== const btnWrap = document.createElement("div"); btnWrap.style.cssText = "display:flex;gap:10px;flex:1;"; const btnNormal = document.createElement("button"); btnNormal.style.cssText = "flex:1;padding:9px;border:none;border-radius:8px;background:#666;color:#fff;"; btnNormal.textContent = "普通HTML"; btnNormal.onclick = async () => { mask.remove(); try { const isWechat = !!document.querySelector('#js_content, .rich_media_content'); const floatSelector = [ '[style*="position:fixed"]','[style*="position:absolute"]', '.floating','.float','.fixed','.sticky','.popup','.modal', '.float-btn','.float-ball','.wx-float','.tool-float','.side-float', '.guide-float','.ads-float','.service-float','.kefu-float', '[class*="ad-"]','[id*="ad-"]','[class*="popup-"]','[id*="popup-"]', '.JQMA-btn-all','.JQMA-btn-del','.JQMA-inner-all','.JQMA-mark-innerBtn','.JQMA-btn-hrefAll','.JQMA-btn-hrefSpan', '.tm-floating-btn','.tm-mobile-menu','.tm-find-overlay','.tm-rule-manager','.tm-replace-dialog-mask', '[class*="float"],[class*="sniff"],[class*="parse"],[class*="download"],#sniffer-main-box,.circular-ring,.star-item' ].join(','); await new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); document.querySelectorAll('img[data-src]').forEach(img => img.src = img.dataset.src); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 25) { clearInterval(interval); window.scrollTo(0, 0); setTimeout(resolve, 700); return; } lastHeight = newHeight; tries++; }, 300); }); const cloneDoc = document.cloneNode(true); cloneDoc.querySelectorAll("script, style, noscript, iframe").forEach(el => el.remove()); cloneDoc.querySelectorAll(floatSelector).forEach(el => el.remove()); if (isWechat) { cloneDoc.querySelectorAll( '.comment,.evaluate,.like,.recommend,.footer,.wx-footer,.msg-footer,.reply-box,.read-more,' + '.article-appreciate,#appreciate,.rich_media_tool_area,#comment_area,#content_bottom_area,.qr_code_pc_outer,.wx-float-window,' + 'p:empty,div:empty,span:empty,br,hr' ).forEach(el => el.remove()); } const htmlStr = `<!DOCTYPE html>\n${cloneDoc.documentElement.outerHTML}`; const blob = new Blob([htmlStr], { type: "text/html;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${title}.html`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); showToast("纯净单文件HTML保存成功", "success"); } catch (e) { showToast("网页保存失败", "error"); } }; const btnBase64 = document.createElement(“button”); btnBase64.style.cssText = “flex:1;padding:9px;border:none;border-radius:8px;background:#2589ff;color:#fff;”; btnBase64.textContent = “Base64内嵌HTML”; btnBase64.onclick = async () => { mask.remove(); try { const isWechat = !!document.querySelector(‘#js_content, .rich_media_content’); const floatSelector = [ ‘[style=”position:fixed”]’,’[style=”position:absolute”]’, ‘.floating’,’.float’,’.fixed’,’.sticky’,’.popup’,’.modal’, ‘.float-btn’,’.float-ball’,’.wx-float’,’.tool-float’,’.side-float’, ‘.guide-float’,’.ads-float’,’.service-float’,’.kefu-float’, ‘[class=”ad-“]’,’[id=”ad-“]’,’[class=”popup-“]’,’[id=”popup-“]’, ‘.JQMA-btn-all’,’.JQMA-btn-del’,’.JQMA-inner-all’,’.JQMA-mark-innerBtn’,’.JQMA-btn-hrefAll’,’.JQMA-btn-hrefSpan’, ‘.tm-floating-btn’,’.tm-mobile-menu’,’.tm-find-overlay’,’.tm-rule-manager’,’.tm-replace-dialog-mask’, ‘[class=”float”],[class=”sniff”],[class=”parse”],[class=”download”],#sniffer-main-box,.circular-ring,.star-item’ ].join(‘,’); //=====替换开始:通用加载全部懒加载图片===== await new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 30) { clearInterval(interval); window.scrollTo(0, 0); setTimeout(resolve, 800); return; } lastHeight = newHeight; tries++; }, 200); }); //批量补齐各类懒加载图片属性 const lazyAttrs = [‘data-src’,’data-original’,’data-lazy’,’data-url’,’data-img’,’data-actualsrc’]; document.querySelectorAll(‘img’).forEach(img => { for(let attr of lazyAttrs){ let val = img.getAttribute(attr); if(val && !img.src.includes(val)){ img.src = val; break; } } //兼容srcset let srcset = img.getAttribute(‘srcset’) || img.getAttribute(‘data-srcset’); if(srcset && (!img.src || img.src.startsWith(‘data:image’))){ img.src = srcset.split(‘ ‘)[0]; } }); await new Promise(r=>setTimeout(r,600)); //=====替换结束===== const cloneDoc = document.cloneNode(true); cloneDoc.querySelectorAll(“script, style, noscript, iframe”).forEach(el => el.remove()); cloneDoc.querySelectorAll(floatSelector).forEach(el => el.remove()); if (isWechat) { cloneDoc.querySelectorAll( ‘.comment,.evaluate,.like,.recommend,.footer,.wx-footer,.msg-footer,.reply-box,.read-more,’ + ‘.article-appreciate,#appreciate,.rich_media_tool_area,#comment_area,#content_bottom_area,.qr_code_pc_outer,.wx-float-window,’ + ‘p:empty,div:empty,span:empty,br,hr’ ).forEach(el => el.remove()); } let imgs = cloneDoc.querySelectorAll(‘img’); let pending = imgs.length; await new Promise(resolve => { if (pending === 0) resolve(); imgs.forEach(img => { let src = img.src; if (!src || src.startsWith(‘data:’)) { pending–; if (pending === 0) resolve(); return; } const canvas = document.createElement(‘canvas’); const ctx = canvas.getContext(‘2d’); const tempImg = new Image(); tempImg.crossOrigin = ‘anonymous’; tempImg.onload = () => { canvas.width = tempImg.width; canvas.height = tempImg.height; ctx.drawImage(tempImg, 0, 0); img.src = canvas.toDataURL(‘image/png’); pending–; if (pending === 0) resolve(); }; tempImg.onerror = () => { pending–; if (pending === 0) resolve(); }; tempImg.src = src; }); }); const htmlStr = <!DOCTYPE html>\n${cloneDoc.documentElement.outerHTML}; const blob = new Blob([htmlStr], { type: “text/html;charset=utf-8” }); const url = URL.createObjectURL(blob); const a = document.createElement(“a”); a.href = url; a.download = ${title}.html; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); showToast(“纯净单文件HTML保存成功”, “success”); } catch (e) { showToast(“网页保存失败”, “error”); } }; btnWrap.appendChild(btnNormal); btnWrap.appendChild(btnBase64); // ==========改动结束========== // 原导出PDF→改名:下载PDF const savePdfBtn = document.createElement(“button”); savePdfBtn.style.cssText = btnStyle + “background:#ef4444;”; savePdfBtn.textContent = “下载PDF”; // 新增打印PDF按钮 const printPdfBtn = document.createElement(“button”); printPdfBtn.style.cssText = btnStyle + “background:#2563eb;”; printPdfBtn.textContent = “打印PDF”; btnWrap.appendChild(savePdfBtn); btnWrap.appendChild(printPdfBtn); // 原来代码里 xxx.appendChild(savePdfBtn); // 改成 xxx.appendChild(btnWrap); const closeBtn = document.createElement(“button”); closeBtn.style.cssText = width:100%;padding:9px;border:none;border-top:1px solid #eee;background:#f8f8f8;color:#666;font-size:13px;; closeBtn.textContent = “取消”; btnWrap1.appendChild(downloadTxtBtn); btnWrap1.appendChild(copyTextBtn); btnWrap2.appendChild(btnWrap); box.appendChild(titleBar); box.appendChild(btnWrap1); box.appendChild(btnWrap2); box.appendChild(closeBtn); mask.appendChild(box); document.body.appendChild(mask); const title = document.title?.trim() || ‘无标题文章’; let content = document.body.innerText.trim(); let saveText = 标题:${title}\n链接:${location.href}\n\n${content}; downloadTxtBtn.onclick = () => { mask.remove(); const floatSelector = [ ‘[style=”position:fixed”]’,’[style=”position:absolute”]’, ‘.floating’,’.float’,’.fixed’,’.sticky’,’.popup’,’.modal’, ‘.float-btn’,’.float-ball’,’.wx-float’,’.tool-float’,’.side-float’, ‘.guide-float’,’.ads-float’,’.service-float’,’.kefu-float’, ‘[class=”ad-“]’,’[id=”ad-“]’,’[class=”popup-“]’,’[id=”popup-“]’, ‘.JQMA-btn-all’,’.JQMA-btn-del’,’.JQMA-inner-all’,’.JQMA-mark-innerBtn’,’.JQMA-btn-hrefAll’,’.JQMA-btn-hrefSpan’, ‘.tm-floating-btn’,’.tm-mobile-menu’,’.tm-find-overlay’,’.tm-rule-manager’,’.tm-replace-dialog-mask’, ‘[class=”float”],[class=”sniff”],[class=”parse”],[class=”download”],#sniffer-main-box,.circular-ring,.star-item’ ].join(‘,’); const backStyle = []; document.querySelectorAll(floatSelector).forEach(el=>{ backStyle.push({dom:el,old:el.style.cssText}); el.style.cssText += ‘;display:none !important;’; }); // 重点:临时重新提取不含悬浮的正文 const tempText = 标题:${document.title?.trim()||"无标题"}\n链接:${location.href}\n\n${document.body.innerText.trim()}; const blob = new Blob([tempText], { type: ‘text/plain;charset=utf-8’ }); const url = URL.createObjectURL(blob); const a = document.createElement(‘a’); a.href = url; a.download = ${title}.txt; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); backStyle.forEach(item=>item.dom.style.cssText = item.old); showToast(“纯文本保存成功!”, “success”); }; copyTextBtn.onclick = async () => { mask.remove(); const floatSelector = [ ‘[style=”position:fixed”]’,’[style=”position:absolute”]’, ‘.floating’,’.float’,’.fixed’,’.sticky’,’.popup’,’.modal’, ‘.float-btn’,’.float-ball’,’.wx-float’,’.tool-float’,’.side-float’, ‘.guide-float’,’.ads-float’,’.service-float’,’.kefu-float’, ‘[class=”ad-“]’,’[id=”ad-“]’,’[class=”popup-“]’,’[id=”popup-“]’, ‘.JQMA-btn-all’,’.JQMA-btn-del’,’.JQMA-inner-all’,’.JQMA-mark-innerBtn’,’.JQMA-btn-hrefAll’,’.JQMA-btn-hrefSpan’, ‘.tm-floating-btn’,’.tm-mobile-menu’,’.tm-find-overlay’,’.tm-rule-manager’,’.tm-replace-dialog-mask’, ‘[class=”float”],[class=”sniff”],[class=”parse”],[class=”download”],#sniffer-main-box,.circular-ring,.star-item’ ].join(‘,’); const backStyle = []; document.querySelectorAll(floatSelector).forEach(el => { backStyle.push({dom:el,oldCss:el.style.cssText}); el.style.cssText += ‘;display:none !important;’; }); // 临时重新获取干净文本 let cleanText = 标题:${document.title?.trim()||"无标题"}\n链接:${location.href}\n\n${document.body.innerText.trim()}; try { await navigator.clipboard.writeText(cleanText); showToast(“内容已复制”, “success”); // 编辑面板 const editMask = document.createElement(“div”); editMask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483652;display:flex;align-items:center;justify-content:center;padding:15px;; const editBox = document.createElement(“div”); editBox.style.cssText = width:90%;max-width:350px;background:#fff;border-radius:12px;padding:15px;; const editTitle = document.createElement(“div”); editTitle.style.cssText = font-size:15px;font-weight:500;margin-bottom:10px;text-align:center;; editTitle.textContent = “编辑文本内容”; const textArea = document.createElement(“textarea”); textArea.style.cssText = width:100%;height:200px;padding:8px;border:1px solid #e5e7eb;border-radius:8px;box-sizing:border-box;font-size:13px;overflow-y:auto;overflow-x:hidden;resize:none;; textArea.value = cleanText; const navBtnWrap = document.createElement(“div”); navBtnWrap.style.cssText = “display:flex;gap:6px;margin:10px 0;”; const makeNavBtn = (txt)=>{let b=document.createElement(“button”);b.style.cssText=”flex:1;padding:6px;border-radius:5px;border:none;background:#eef1f5;color:#222;font-size:12px;”;b.textContent=txt;return b;}; const btnTop=makeNavBtn(“顶部”),btnUp=makeNavBtn(“上翻”),btnDown=makeNavBtn(“下翻”),btnBottom=makeNavBtn(“底部”); navBtnWrap.append(btnTop,btnUp,btnDown,btnBottom); btnTop.onclick=()=>textArea.scrollTop=0; btnBottom.onclick=()=>textArea.scrollTop=textArea.scrollHeight; btnUp.onclick=()=>textArea.scrollTop-=textArea.clientHeight0.8; btnDown.onclick=()=>textArea.scrollTop+=textArea.clientHeight0.8; const btnRow = document.createElement(“div”); btnRow.style.cssText = display:flex;gap:8px;margin-top:5px;; const saveLocalBtn=document.createElement(“button”); saveLocalBtn.style.cssText=flex:1;padding:8px;border:none;border-radius:6px;background:#10b981;color:#fff;; saveLocalBtn.textContent=”重新保存文件”; const exitBtn=document.createElement(“button”); exitBtn.style.cssText=flex:1;padding:8px;border:1px solid #ddd;border-radius:6px;background:#fff;; exitBtn.textContent=”完成关闭”; btnRow.appendChild(saveLocalBtn);btnRow.appendChild(exitBtn); editBox.append(editTitle,textArea,navBtnWrap,btnRow); editMask.appendChild(editBox); document.body.appendChild(editMask); textArea.focus(); const restoreFloat=()=>{backStyle.forEach(v=>v.dom.style.cssText=v.oldCss);editMask.remove();}; exitBtn.onclick=restoreFloat; editMask.onclick=e=>e.target===editMask&&restoreFloat(); saveLocalBtn.onclick=function(){ const newTxt=textArea.value; let dropBox=this.parentElement.querySelector(‘.inner-save-dropdown’); if(dropBox){dropBox.remove();return;} dropBox=document.createElement(‘div’);dropBox.className=’inner-save-dropdown’; dropBox.style.cssText=margin-top:6px;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.12);background:#fff;; const itemBase=display:block;width:100%;padding:9px 12px;border:none;background:transparent;font-size:13px;text-align:center;cursor:pointer;color:#333;; const btnJson=document.createElement(‘button’),btnTxt=document.createElement(‘button’),btnMd=document.createElement(‘button’); btnJson.style.cssText=itemBase;btnJson.textContent=”JSON规则”; btnTxt.style.cssText=itemBase+”border-top:1px solid #f0f0f0;”;btnTxt.textContent=”TXT纯文本”; btnMd.style.cssText=itemBase+”border-top:1px solid #f0f0f0;”;btnMd.textContent=”Markdown”; [btnJson,btnTxt,btnMd].forEach(b=>{b.onmouseover=()=>b.style.background=”#f5f7fa”;b.onmouseout=()=>b.style.background=”transparent”;}); dropBox.append(btnJson,btnTxt,btnMd);this.after(dropBox); const down=(name,type)=>{dropBox.remove();const blob=new Blob([newTxt],{type});const a=document.createElement(‘a’);a.href=URL.createObjectURL(blob);a.download=name;a.click();a.remove();showToast(“保存成功”,”success”);}; btnJson.onclick=()=>down(“规则.json”,”application/json”); btnTxt.onclick=()=>down(“文本.txt”,”text/plain”); btnMd.onclick=()=>down(“文档.md”,”text/markdown”); }; } catch (err) { backStyle.forEach(v=>v.dom.style.cssText=v.oldCss); const ta=document.createElement(‘textarea’);ta.value=cleanText;ta.style.left=”-9999px”;document.body.appendChild(ta);ta.select();document.execCommand(‘copy’);ta.remove(); showToast(“内容复制完成”, “success”); } }; // 4.【下载PDF按钮:原有老逻辑】 savePdfBtn.onclick = async () => { if (isPrinting) return; isPrinting = true; mask?.remove(); const isWechat = !!document.querySelector(‘#js_content, .rich_media_content’); // 超级选择器:类名 + 内联style双杀,追加手机助手悬浮控件 const floatSelector = [ ‘[style=”position:fixed”]’,’[style=”position:absolute”]’, ‘.floating’,’.float’,’.fixed’,’.sticky’,’.popup’,’.modal’, ‘.float-btn’,’.float-ball’,’.wx-float’,’.tool-float’,’.side-float’, ‘.guide-float’,’.ads-float’,’.service-float’,’.kefu-float’, ‘[class=”ad-“]’,’[id=”ad-“]’,’[class=”popup-“]’,’[id=”popup-“]’, // 追加:手机助手全部悬浮按钮/面板 ‘.JQMA-btn-all’,’.JQMA-btn-del’,’.JQMA-inner-all’,’.JQMA-mark-innerBtn’,’.JQMA-btn-hrefAll’,’.JQMA-btn-hrefSpan’ ].join(‘,’); // 1. 前置:强制隐藏自身+第三方+手机助手悬浮,删DOM+清样式 const killAllFloat = () => { // 删自身原有按钮 document.querySelectorAll(‘.tm-floating-btn, #float-temp-btn’).forEach(el => el.remove()); // 删 手机助手 所有悬浮控件 document.querySelectorAll(‘.JQMA-btn-all, .JQMA-btn-del, .JQMA-inner-all, .JQMA-mark-innerBtn, .JQMA-btn-hrefAll, .JQMA-btn-hrefSpan’).forEach(el => el.remove()); // 删第三方(含内联fixed) document.querySelectorAll(floatSelector).forEach(el => { el.remove(); el.style.cssText = ‘display:none !important; visibility:hidden !important; opacity:0 !important;’; }); // 绝杀:抓所有带fixed/absolute内联样式的元素 document.querySelectorAll(‘’).forEach(el => { if (el.style.position === ‘fixed’ || el.style.position === ‘absolute’) { el.remove(); } }); }; // 2. 防再生:打印前持续删3秒 const startKillInterval = () => { killAllFloat(); return setInterval(killAllFloat, 300); // 每300ms删一次 }; const killTimer = startKillInterval(); if (isWechat) { const filterUselessContent = () => { // 清理公众号冗余 document.querySelectorAll( ‘.comment, .evaluate, .like, .recommend, .footer, .wx-footer, .msg-footer, .reply-box, .read-more,’ + ‘.article-appreciate, #appreciate, .rich_media_tool_area, #comment_area, #content_bottom_area,’ + ‘.qr_code_pc_outer, .wx-float-window’ ).forEach(el => el.remove()); document.querySelectorAll(‘p:empty, div:empty, span:empty, br, hr’).forEach(el => el.remove()); document.body.style.cssText = ‘margin:0 !important; padding:0 !important; padding-bottom:0 !important;’; }; const clearFixedHeight = () => { document.documentElement.style.cssText = ‘height:auto !important; overflow:visible !important; margin:0 !important; padding:0 !important;’; document.body.style.cssText = ‘height:auto !important; overflow:visible !important; margin:0 !important; padding:0 !important;’; document.querySelectorAll(‘.rich_media_content, #js_content’).forEach(el => { el.style.cssText = ‘height:auto !important; max-height:none !important; overflow:visible !important;’; el.style.pageBreakInside = ‘avoid’; el.style.pageBreakAfter = ‘avoid’; }); }; const scrollToBottom = () => { return new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); document.querySelectorAll(‘img[data-src]’).forEach(img => img.src = img.dataset.src); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 25) { clearInterval(interval); window.scrollTo(0, 0); setTimeout(resolve, 800); return; } lastHeight = newHeight; tries++; }, 300); }); }; filterUselessContent(); clearFixedHeight(); await scrollToBottom(); const printStyle = document.createElement(‘style’); printStyle.textContent = @page { size: A4; margin: 5mm !important; orphans:1; widows:1; } @media print { html,body {height:auto !important; overflow:visible !important; margin:0 !important; padding:0 !important; zoom:0.95 !important;} .rich_media_content, #js_content {page-break-inside:avoid !important; margin:0 !important; padding:0 !important;} /* 终极样式绝杀:所有浮窗+手机助手控件全干掉 */ ${floatSelector}, .tm-floating-btn, #float-temp-btn, [style*="position:fixed"], [style*="position:absolute"] { display:none !important; visibility:hidden !important; opacity:0 !important; width:0 !important; height:0 !important; left:-9999px !important; top:-9999px !important; position:absolute !important; z-index:-99999 !important; transform:scale(0) !important; pointer-events:none !important; } p:empty,div:empty,span:empty,br,hr {display:none !important; height:0 !important;} }; document.head.appendChild(printStyle); setTimeout(() => { clearInterval(killTimer); // 停止定时删除 killAllFloat(); // 最后再清一次 window.print(); setTimeout(() => { printStyle.remove(); location.reload(); }, 1500); }, 5000); } else { // ====================== 普通网页 整套逻辑 ====================== // 提前主动隐藏所有悬浮控件,避免缩放挤压变形 const hideAllFloatPanel = () => { // 原有助手悬浮控件 document.querySelectorAll( ‘.tm-floating-btn,.tm-mobile-menu,.JQMA-btn-all,.JQMA-btn-del,.JQMA-inner-all,.JQMA-mark-innerBtn’ ).forEach(el => { el.style.display = ‘none’; el.style.visibility = ‘hidden’; }); // 布局美化、图片操作类悬浮框 + 所有固定绝对定位元素 document.querySelectorAll( ‘[style=”position:fixed”],[style=”position:absolute”],[style=”position:absolute”],[style=”position: fixed”],.floating,.fixed,.sticky,.popup,.modal’ ).forEach(el => { el.style.display = ‘none’; el.style.visibility = ‘hidden’; el.style.opacity = ‘0’; }); }; // 1. 滚到底触发懒加载 const scrollToBottom = () => { return new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); // 加载懒加载图片 document.querySelectorAll(‘img[data-src],img[data-original]’).forEach(img => { const src = img.dataset.src || img.dataset.original; if (src) img.src = src; }); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 15) { clearInterval(interval); window.scrollTo(0, 0); resolve(); } lastHeight = newHeight; tries++; }, 300); }); }; // 2. 清固定高度、防止卡死 const clearFixedHeight = () => { document.documentElement.style.cssText = ‘height:auto !important; overflow:visible !important;’; document.body.style.cssText = ‘height:auto !important; overflow:visible !important;’; document.querySelectorAll(‘[style]’).forEach(el => { const s = el.style; if (s.height === ‘100vh’ || s.height === ‘100%’ || s.overflow === ‘hidden’) { el.style.cssText = ‘’; } }); document.querySelectorAll(‘div, section, main, .container, .wrapper, .content’).forEach(el => { el.style.height = ‘auto’; el.style.maxHeight = ‘none’; el.style.minHeight = ‘0’; el.style.overflow = ‘visible’; }); }; // 先执行隐藏、清高度、加载完整页面 hideAllFloatPanel(); clearFixedHeight(); await scrollToBottom(); // 3. 打印样式 纯CSS强化防空白页 const printHideStyle = document.createElement(‘style’); printHideStyle.id = ‘tm-print-hide-float’; printHideStyle.textContent = ` @page { size: A4; margin: 10mm; zoom: 0.99; /* 强制单页延续,禁止自动分页 / page-break: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; orphans: 1; widows: 1; } @media print { html, body { height: auto !important; max-height: none !important; overflow: visible !important; width: 210mm !important; margin: 0 auto !important; padding: 0 !important; font-family: “SimSun”, “Microsoft YaHei”, “Source Han Sans”, sans-serif !important; background: #fff !important; / 全局禁止分页 / page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } body { transform: scale(0.99) !important; transform-origin: top center !important; width: 100% !important; } / 所有内容容器:彻底锁死分页 / div, section, main, .container, .wrapper, .content { page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; margin: 0 !important; padding: 0 !important; } p, span, li, h1, h2, h3, h4, h5, h6 { color: #000 !important; margin: 0.2rem 0 !important; padding: 0 !important; page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } / 图片核心规则:图片之间绝对不分页 / img { max-width: 95% !important; height: auto !important; display: block !important; margin: 0.2rem auto !important; padding: 0 !important; page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } / 清除所有隐形空白元素 / p:empty, div:empty, span:empty, br, hr, .line, .divider { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; line-height: 0 !important; } / 隐藏悬浮控件 / .tm-floating-btn, .tm-mobile-menu, .JQMA-btn-all, .JQMA-btn-del, .JQMA-inner-all, .JQMA-mark-innerBtn, [style=”position:fixed”], [style=”position:absolute”], .floating,.fixed,.sticky,.popup,.modal { display: none !important; visibility: hidden !important; } } `; document.head.appendChild(printHideStyle); // 加长等待时间到5秒 setTimeout(() => { window.print(); setTimeout(() => { document.getElementById(‘tm-print-hide-float’)?.remove(); isPrinting = false; }, 1500); }, 5000); } }; // ==========新按钮:打印PDF 完整逻辑(表格竖排修复版)========== printPdfBtn.onclick = async () => { if (isPrinting) return; isPrinting = true; mask?.remove(); // 识别是否为微信公众号文章页面 const isWechat = !!document.querySelector(‘#js_content, .rich_media_content’); if (isWechat) { // 隐藏悬浮控件、固定栏 const hideAllFloatPanel = () => { document.querySelectorAll( ‘.tm-floating-btn,.tm-mobile-menu,.JQMA-btn-all,.JQMA-btn-del,.JQMA-inner-all,.JQMA-mark-innerBtn’ ).forEach(el => { el.style.display = ‘none’; el.style.visibility = ‘hidden’; }); document.querySelectorAll( ‘[style=”position:fixed”],[style=”position:absolute”],.floating,.fixed,.sticky,.popup,.modal’ ).forEach(el => { el.style.display = ‘none’; el.style.visibility = ‘hidden’; el.style.opacity = ‘0’; }); }; // 过滤顶部多余线条、底部评价/留言、公众号菜单等无关区块 const filterUselessContent = () => { document.querySelectorAll(‘hr, .line, .divider, .top-bar, .header’).forEach(el => { el.style.display = ‘none’; }); document.querySelectorAll(‘.comment, .evaluate, .like, .recommend, .footer, .wx-footer, .msg-footer, .reply-box, .read-more’).forEach(el => { el.style.display = ‘none’; el.style.height = ‘0’; el.style.overflow = ‘hidden’; }); }; // 滚动加载全部核心图文,强制渲染首屏 const scrollToBottom = () => { return new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); const contentBox = document.querySelector(‘#js_content,.rich_media_content’); if(contentBox){ contentBox.scrollTop = contentBox.scrollHeight; } document.querySelectorAll(‘[class=”swiper”],[class*=”slide”]’).forEach(el => { el.style.overflow = ‘visible’; el.style.width = ‘auto’; el.style.display = ‘block’; }); document.querySelectorAll(‘img[data-src]’).forEach(img => { if(img.dataset.src) img.src = img.dataset.src; }); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 25) { clearInterval(interval); window.scrollTo(0, 0); setTimeout(resolve, 800); return; } lastHeight = newHeight; tries++; }, 300); }); }; // 清除样式限制,消除上下留白 const clearFixedHeight = () => { document.documentElement.style.cssText = ‘height:auto !important; overflow:visible !important;’; document.body.style.cssText = ‘height:auto !important; overflow:visible !important; margin:0 !important; padding:0 !important;’; // 强制清空底部留白(防空白页) document.body.style.marginBottom = ‘0’; document.body.style.paddingBottom = ‘0’; document.querySelectorAll('*[style]').forEach(el => { const s = el.style; if (s.height === '100vh' || s.overflow === 'hidden' || s.maxHeight) { el.style.height = ''; el.style.maxHeight = ''; el.style.overflow = ''; } }); document.querySelectorAll('div, section, main, .rich_media_content, #js_content').forEach(el => { el.style.height = 'auto'; el.style.maxHeight = 'none'; el.style.overflow = 'visible'; el.style.width = '100%'; el.style.maxWidth = 'none'; el.style.position = 'static'; el.style.display = 'block'; el.style.margin = '0'; el.style.padding = '0'; // 清空底部边距 el.style.marginBottom = '0'; el.style.paddingBottom = '0'; }); }; // 执行初始化操作 hideAllFloatPanel(); filterUselessContent(); clearFixedHeight(); await scrollToBottom(); // 👇👇👇 专门处理微信扫码/悬浮弹窗(解决打印时才冒出来的问题)👇👇👇 setTimeout(() => { // 隐藏所有微信弹窗、悬浮控件 document.querySelectorAll('.wx-float, .wx-popup, .wx-dialog, .weui-dialog, .modal, .float, .dialog, [class*="wechat"]').forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; el.style.opacity = '0'; el.remove(); }); // 针对“微信扫一扫可打开此内容”的弹窗 document.querySelectorAll('[style*="position:fixed"], [style*="position:absolute"]').forEach(el => { if(el.innerText.includes('微信扫一扫') || el.innerText.includes('使用完整服务')) { el.style.display = 'none'; el.remove(); } }); }, 300); // 删除空元素、换行标签,清除隐形占位,杜绝空白页 document.querySelectorAll('p:empty, div:empty, span:empty, br').forEach(el => { el.remove(); }); // 微信文章:强制加载所有懒加载图 document.querySelectorAll('img[data-src]').forEach(img => { if (img.dataset.src && (!img.src || img.src.startsWith('data:'))) { img.src = img.dataset.src; } }); // 图片基础样式 + 水平居中预处理(文字不居中) const contentBox = document.querySelector('#js_content,.rich_media_content'); if(contentBox){ contentBox.style.margin = '0 auto'; } document.querySelectorAll('img').forEach(img => { img.style.maxHeight = 'none'; img.style.maxWidth = '95%'; img.style.margin = '0.3rem auto'; img.style.display = 'block'; }); // 打印样式:修复表格竖排问题,恢复正常行列结构 const printHideStyle = document.createElement('style'); printHideStyle.id = 'tm-print-hide-float'; printHideStyle.textContent = ` @page { size: A4 portrait; margin: 8mm !important; orphans: 1; widows: 1; margin-bottom: 2mm !important; } @media print { html, body { zoom: 0.99 !important; } html, body { height: auto !important; max-height: none !important; overflow: visible !important; width: 100% !important; margin: 0 !important; padding: 0 !important; background: #fff !important; } /* 注意:这里排除了table相关元素,不强制display:block,避免表格结构被打散 */ .rich_media_content, .rich_media_content :not(table):not(thead):not(tbody):not(tr):not(th):not(td), #js_content, #js_content :not(table):not(thead):not(tbody):not(tr):not(th):not(td) { display: block !important; opacity: 1 !important; overflow: visible !important; height: auto !important; max-height: none !important; max-width: none !important; color: #000 !important; background: transparent !important; float: none !important; position: static !important; clip: auto !important; clip-path: none !important; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } .rich_media_content img, #js_content img { display: block !important; max-width: 95% !important; width: auto !important; height: auto !important; max-height: 96vh !important; object-fit: contain !important; margin: 0.3rem auto !important; } img { max-width: 95% !important; height: auto !important; display: block !important; margin: 0 auto !important; } p, span, li, h1, h2, h3, h4, h5, h6, em, strong { text-align: left !important; margin: 0.5rem 0 !important; line-height: 1.7 !important; color: #000 !important; visibility: visible !important; } ul, ol, li { display: block !important; margin-left: 1rem !important; visibility: visible !important; } body::before, body::after { content: none !important; display: none !important; } div, section, article, main, .rich_media_content { margin-bottom: 0 !important; padding-bottom: 0 !important; } /* ========== 表格修复:恢复正常行列结构 ========== */ table { display: table !important; width: 100% !important; max-width: 100% !important; border-collapse: collapse !important; table-layout: auto !important; margin: 0.8rem 0 !important; page-break-inside: avoid !important; } thead, tbody { display: table-header-group, table-row-group !important; } tr { display: table-row !important; page-break-inside: avoid !important; } th, td { display: table-cell !important; border: 1px solid #333 !important; padding: 0.4rem 0.5rem !important; text-align: center !important; vertical-align: middle !important; word-wrap: break-word !important; word-break: break-all !important; white-space: normal !important; } th { background-color: #2a5f8f !important; color: #fff !important; font-weight: bold !important; } td { text-align: left !important; } /* 表格内文本适配 */ table p, table span { margin: 0.1rem 0 !important; line-height: 1.5 !important; } /* 原有隐藏项 */ .tm-floating-btn, .JQMA-btn-all, .JQMA-btn-del, [style*="position:fixed"], .comment,.footer,.reply-box,.wx-footer,.msg-footer,.read-more { display: none !important; height: 0 !important; overflow: hidden !important; } p:empty, div:empty, span:empty, br, hr { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } hr, .line, .divider, .comment, .evaluate, .footer, .wx-footer, .reply-box, .top-bar { display: none !important; } .tm-floating-btn, .tm-mobile-menu, .JQMA-btn-all, .JQMA-btn-del, .JQMA-inner-all, .JQMA-mark-innerBtn, [style*="position:fixed"], [style*="position:absolute"], .floating,.fixed,.sticky,.popup,.modal { display: none !important; visibility: hidden !important; } /* ========== 微信公众号专属:全覆盖隐藏多余模块 ========== */ /* 1. 打赏、点赞、在看、投票 */ .reward, .donate, .reward_box, .dianzan, .like, .like-area, .watch, .look_num, .vote, .vote-box, .praise, .post-like { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } /* 2. 留言、评论、消息区 */ #comment, .comment-area, .comment-list, .comment-input, .msg-container, .message-box, .leave-message, .discuss { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } /* 3. 作者信息、文章头部/尾部元数据 */ .author, .author-name, .author-desc, .article-info, .meta, .art_meta, .publish-time, .source, .copyright { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } /* 4. 相关推荐、广告、公众号二维码 */ .related, .related-article, .recommend-list, .ad, .advert, .qrcode, .qr-code, .wechat-qr, .official-qrcode { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } /* 5. 底部工具栏、分享、互动、导航栏 */ .tool-bar, .bottom-bar, .share, .share-box, .interact, .nav-bar, .footer-tool, .float-tool, .wx-tool { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } /* 兜底:微信文章底部整块附加区域全部隐藏 */ #js_bottom, .rich_media_bottom, .article-bottom, .extra-box { display: none !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } } `; document.head.appendChild(printHideStyle); setTimeout(() => { window.print(); setTimeout(() => { document.getElementById('tm-print-hide-float')?.remove(); isPrinting = false; }, 1500); }, 1800); }else { // ====================== 普通网页 整套逻辑 ====================== // 提前主动隐藏所有悬浮控件,避免缩放挤压变形 const hideAllFloatPanel = () => { // 原有助手悬浮控件 document.querySelectorAll( '.tm-floating-btn,.tm-mobile-menu,.JQMA-btn-all,.JQMA-btn-del,.JQMA-inner-all,.JQMA-mark-innerBtn' ).forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; }); // 布局美化、图片操作类悬浮框 + 所有固定绝对定位元素 document.querySelectorAll( '[style*="position:fixed"],[style*="position:absolute"],[style*="position:absolute"],[style*="position: fixed"],.floating,.fixed,.sticky,.popup,.modal' ).forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; el.style.opacity = '0'; }); }; // 1. 滚到底触发懒加载 const scrollToBottom = () => { return new Promise(resolve => { let lastHeight = document.body.scrollHeight; let tries = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); // 加载懒加载图片 document.querySelectorAll('img[data-src],img[data-original]').forEach(img => { const src = img.dataset.src || img.dataset.original; if (src) img.src = src; }); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight || tries > 15) { clearInterval(interval); window.scrollTo(0, 0); resolve(); } lastHeight = newHeight; tries++; }, 300); }); }; // 2. 清固定高度、防止卡死 const clearFixedHeight = () => { document.documentElement.style.cssText = 'height:auto !important; overflow:visible !important;'; document.body.style.cssText = 'height:auto !important; overflow:visible !important;'; document.querySelectorAll('*[style]').forEach(el => { const s = el.style; if (s.height === '100vh' || s.height === '100%' || s.overflow === 'hidden') { el.style.cssText = ''; } }); document.querySelectorAll('div, section, main, .container, .wrapper, .content').forEach(el => { el.style.height = 'auto'; el.style.maxHeight = 'none'; el.style.minHeight = '0'; el.style.overflow = 'visible'; }); }; // 先执行隐藏、清高度、加载完整页面 hideAllFloatPanel(); clearFixedHeight(); await scrollToBottom(); // 3. 打印样式 纯CSS强化防空白页 const printHideStyle = document.createElement('style'); printHideStyle.id = 'tm-print-hide-float'; printHideStyle.textContent = ` @page { size: A4; margin: 10mm; zoom: 0.99; /* 强制单页延续,禁止自动分页 */ page-break: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; orphans: 1; widows: 1; } @media print { html, body { height: auto !important; max-height: none !important; overflow: visible !important; width: 210mm !important; margin: 0 auto !important; padding: 0 !important; font-family: "SimSun", "Microsoft YaHei", "Source Han Sans", sans-serif !important; background: #fff !important; /* 全局禁止分页 */ page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } body { transform: scale(0.99) !important; transform-origin: top center !important; width: 100% !important; } /* 所有内容容器:彻底锁死分页 */ div, section, main, .container, .wrapper, .content { page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; margin: 0 !important; padding: 0 !important; } p, span, li, h1, h2, h3, h4, h5, h6 { color: #000 !important; margin: 0.2rem 0 !important; padding: 0 !important; page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } /* 图片核心规则:图片之间绝对不分页 */ img { max-width: 95% !important; height: auto !important; display: block !important; margin: 0.2rem auto !important; padding: 0 !important; page-break-inside: avoid !important; page-break-before: avoid !important; page-break-after: avoid !important; } /* 清除所有隐形空白元素 */ p:empty, div:empty, span:empty, br, hr, .line, .divider { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; line-height: 0 !important; } /* 隐藏悬浮控件 */ .tm-floating-btn, .tm-mobile-menu, .JQMA-btn-all, .JQMA-btn-del, .JQMA-inner-all, .JQMA-mark-innerBtn, [style*="position:fixed"], [style*="position:absolute"], .floating,.fixed,.sticky,.popup,.modal { display: none !important; visibility: hidden !important; } } `; document.head.appendChild(printHideStyle); // 加长等待时间到5秒 setTimeout(() => { window.print(); setTimeout(() => { document.getElementById('tm-print-hide-float')?.remove(); isPrinting = false; }, 1500); }, 5000); } }; closeBtn.onclick = () => mask.remove(); mask.onclick = e => { if(e.target === mask) mask.remove(); }; } function updateRulesAndApply(newRules) { const oldRules = JSON.stringify(replaceRules); const newRulesStr = JSON.stringify(newRules); if (oldRules === newRulesStr) { showToast(“规则无变化”, “info”); return; } replaceRules = newRules; GM_setValue(‘saveReplaceRules’, newRules); // 只存规则,无论开没开替换,都不自动重载、不打断正在替换 showToast(“规则已保存,点重载规则生效”, “success”); } function addMultipleRules(fromText, toText) { if (!fromText || !toText) { showToast(“请填写完整”, “error”); return false; } const fromLines = fromText.split(‘\n’).map(line => line.trim()).filter(line => line); const toLines = toText.split(‘\n’).map(line => line.trim()).filter(line => line); if (fromLines.length === 0 || toLines.length === 0 || fromLines.length !== toLines.length) { showToast(“行数不一致或内容为空”, “error”); return false; } const newRules = […replaceRules]; let addedCount = 0; for (let i = 0; i < fromLines.length; i++) { const from = fromLines[i]; const to = toLines[i]; if (from && to && from !== to && !newRules.some(rule => rule.from === from && rule.to === to)) { newRules.push({ from, to, enabled: true }); addedCount++; } } if (addedCount === 0) { showToast(“没有新增规则”, “info”); return false; } updateRulesAndApply(newRules); showToast(已添加 ${addedCount} 条规则, “success”); return true; } function showToast(message, type = “info”) { const colors = {success: “#10b981”,info: “#3b82f6”,warning: “#f59e0b”,error: “#ef4444”}; const existing = document.querySelector(‘.tm-toast’); if (existing) existing.remove(); const toast = document.createElement(‘div’); toast.className = ‘tm-toast’; toast.textContent = message; toast.style.cssText = ` position:fixed; top:auto; bottom:12%; left:50%; transform:translate(-50%,0); background:${colors[type]}; color:#fff; padding:12px 20px; border-radius:12px; font-size:15px; font-weight:bold; z-index:2147483647; opacity:0; transition:opacity 0.3s,transform 0.3s; box-shadow:0 4px 12px rgba(0,0,0,0.15); white-space:nowrap; max-width:80vw; overflow:hidden; text-overflow:ellipsis; `; document.body.appendChild(toast); requestAnimationFrame(()=>{toast.style.opacity=’1’;toast.style.transform=’translate(-50%,0)’;}); setTimeout(()=>{ toast.style.opacity=’0’;toast.style.transform=’translate(-50%,-20px)’; setTimeout(()=>toast.remove(),300); },2000); } function createFloatingButton() { if (document.querySelector(‘.tm-floating-btn’)) return; const savedPos = localStorage.getItem(‘tmFloatPos’); let initialPos = { x: window.innerWidth - 46, y: window.innerHeight / 2 }; if (savedPos) { try { const pos = JSON.parse(savedPos); initialPos = {x: Math.min(pos.x, window.innerWidth - 46),y: Math.min(pos.y, window.innerHeight - 50)}; } catch (e) {} } const btn = document.createElement(‘div’); btn.className = ‘tm-floating-btn’; btn.style.cssText = ` position:fixed;left:${initialPos.x}px;top:${initialPos.y}px;width:46px;height:46px;border-radius:23px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;font-size:12px;font-weight:bold; display:flex;align-items:center;justify-content:center;z-index:2147483646;cursor:move; user-select:none;touch-action:none;box-shadow:0 4px 15px rgba(0,0,0,0.1); transition:all 0.25s cubic-bezier(0.4,0,0.2,1);border:2px solid rgba(255,255,255,0.2); opacity:0.28;transform:translateX(0) scale(1); `; const icon = document.createElement(‘div’); icon.textContent = isReplacing ? ‘🔁’ : ‘⚪’; icon.style.cssText = ‘font-size:20px !important;transition:transform 0.3s;’; btn.appendChild(icon); let lastScrollY = window.scrollY; let scrollTimer = null; const handleScroll = () => { clearTimeout(scrollTimer); const currentScrollY = window.scrollY; if (Math.abs(currentScrollY - lastScrollY) > 3) { btn.style.transform = ‘translateX(36px) scale(0.5)’; btn.style.opacity = ‘0.14’; } scrollTimer = setTimeout(() => { btn.style.transform = ‘translateX(0) scale(1)’; btn.style.opacity = ‘0.28’; }, 800); lastScrollY = currentScrollY; }; window.addEventListener(‘scroll’, handleScroll, { passive: true }); document.addEventListener(‘touchmove’, handleScroll, { passive: true }); window.updateFloatingButton = () => { icon.textContent = isReplacing ? ‘🔁’ : ‘⚪’; btn.style.background = isReplacing ? ‘linear-gradient(135deg, rgba(16,185,129,0.35) 0%, rgba(5,150,105,0.35) 100%)’ ‘linear-gradient(135deg, rgba(102,126,234,0.35) 0%, rgba(118,75,162,0.35) 100%)’; }; let startX, startY, initialX, initialY; btn.addEventListener(‘touchstart’, (e) => { e.preventDefault();e.stopPropagation(); const touch = e.touches[0]; startX = touch.clientX;startY = touch.clientY; initialX = parseInt(btn.style.left);initialY = parseInt(btn.style.top); isDragging = false; longPressTimer = setTimeout(()=>{isLongPress=true;showMobileMenu();},300); }); btn.addEventListener(‘touchmove’, (e) => { e.preventDefault();e.stopPropagation(); if(longPressTimer) clearTimeout(longPressTimer); const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; if(Math.abs(deltaX)>8||Math.abs(deltaY)>8){isDragging=true;isLongPress=false;} if(isDragging){ let newX = initialX + deltaX; let newY = initialY + deltaY; newX = Math.max(10, Math.min(newX, window.innerWidth - 46)); newY = Math.max(10, Math.min(newY, window.innerHeight - 46)); btn.style.left = newX+’px’;btn.style.top=newY+’px’; btn.style.transform=’scale(1.1)’; } }); btn.addEventListener(‘touchend’, (e) => { e.preventDefault();e.stopPropagation(); if(longPressTimer) clearTimeout(longPressTimer); if(isDragging){ isDragging=false;btn.style.transform=’scale(1)’; const rect = btn.getBoundingClientRect(); localStorage.setItem(‘tmFloatPos’,JSON.stringify({x:rect.left,y:rect.top})); }else if(isLongPress){ isLongPress=false; }else{ const now = Date.now(); if(now-lastTapTime<400){clearTimeout(tapTimer);toggleReplace();} else{tapTimer=setTimeout(()=>openReplaceDialog(),220);} lastTapTime=now; } }); const removeScrollListener = () => { window.removeEventListener(‘scroll’, handleScroll); document.removeEventListener(‘touchmove’, handleScroll); }; btn.addEventListener(‘remove’, removeScrollListener); document.body.appendChild(btn); updateFloatingButton(); } function showMobileMenu() { const existing = document.querySelector(‘.tm-mobile-menu’); if (existing) existing.remove(); calcEnabledRuleTotal(); const menu = document.createElement(‘div’); menu.className = ‘tm-mobile-menu’; menu.style.cssText = position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7); z-index:2147483645;display:flex;flex-direction:column;justify-content:flex-end;backdrop-filter:blur(5px);; const content = document.createElement(‘div’); content.style.cssText = background:#fff;border-radius:14px 14px 0 0;padding:8px;max-height:50vh;margin:0 22px;overflow-y:auto;; function renderMainBtn(){ const status = isReplacing; let btn = content.querySelector(‘.main-switch-btn’); if(!btn){btn=document.createElement(‘div’);btn.className=’main-switch-btn’;content.prepend(btn);} btn.style.cssText = display:flex;align-items:center;padding:9px;margin:3px 0;background:#f9fafb;border-radius:8px;font-size:13px;color:#1f2937;cursor:pointer;; btn.innerHTML = <span style="font-size:15px;margin-right:2px;">${status?'⏸️':'▶️'}</span><span style="color:${status?'red':'green'};">${status?'停止替换':'开始替换'}</span>; btn.onclick = () => {toggleReplace();renderMainBtn();}; } renderMainBtn(); let highlightDuration = GM_getValue(‘highlightDuration’, 5); function renderHighlightBtn() { let btn = content.querySelector(‘.highlight-switch-btn’); if (!btn) { btn = document.createElement(‘div’); btn.className = ‘highlight-switch-btn’; content.appendChild(btn); } btn.style.cssText = display:flex;align-items:center;justify-content:space-between;padding:9px;margin:3px 0;background:#f9fafb;border-radius:8px;font-size:13px;color:#1f2937;cursor:pointer;; btn.innerHTML = <div style="display:flex;align-items:center;"> <span style="font-size:15px;margin-right:2px;">💡</span> <span>文本高亮</span> </div> <div style="display:flex;align-items:center;gap:8px;"> <span style="font-size:12px;color:#666;">${highlightEnabled ? 开启 (${highlightDuration}s) : '关闭'}</span> <div style="width:36px;height:20px;border-radius:10px;background:${highlightEnabled ? '#10b981' : '#ddd'};transition:background 0.3s;"></div> <span class="set-duration" style="font-size:11px;color:#888;cursor:pointer;">⏱️设置</span> </div>; btn.onclick = function(e) { if (e.target.classList.contains(‘set-duration’)) return; toggleHighlight(); renderHighlightBtn(); }; const setBtn = btn.querySelector(‘.set-duration’); setBtn.onclick = function(e) { e.stopPropagation(); const val = prompt(“高亮持续时间(秒,0=永久显示)”, highlightDuration); if (val !== null) { const num = Number(val); if (!isNaN(num) && num >= 0) { highlightDuration = num; GM_setValue(‘highlightDuration’, highlightDuration); renderHighlightBtn(); } } }; } renderHighlightBtn(); // 重载规则函数 function forceReapplyAllRules() { // 先标记原先状态 const tempRun = isReplacing; restoreOriginalKeepRules(); // 原本是开启就重扫 if (tempRun && document.body) { scan(document.body); } // 还原运行状态,防止变成关闭 isReplacing = tempRun; window.updateFloatingButton(); showToast(“已重载全部替换规则”, ‘success’); } // ========== TXT查看器(标记时实时识别标题栏高度) ========== (function() { let fileInput = null; let mask = null; let txtBox = null; let wrapDom = null; let currentFileName = “text.txt”; let markBar = null; let markIcon = null; let topBtn = null; let titleBar = null; let lastText = localStorage.getItem('txtLastContent') || ''; let currentFontSize = localStorage.getItem('txtFontSize') || '16px'; let isDarkMode = localStorage.getItem('txtDarkMode') === 'true'; // 原底色 + 新增3个指定配色 const bgColorList = [ '#ffffff', '#f5f5f5', '#fff8e1', '#e8f4f8', '#fef2f2', '#C0EEC6', // #FFC0EEC6 '#ABCEE0', // #FFABCEE0 '#C2D8A9' // #FFC2D8A9 ]; let bgIndex = parseInt(localStorage.getItem('txtBgIndex')) || 0; const step = 1; // 同时记录:滚动位置 + 标记当时的标题栏高度 let markScrollTop = parseInt(localStorage.getItem('txtMarkPos') || '0'); let markBarHeight = parseInt(localStorage.getItem('txtMarkBarH') || '0'); let lastScrollTop = 0; let scrollTimeout = null; function showBtnTip(btn, tipText, originIcon) { btn.innerText = tipText; setTimeout(() => { btn.innerText = originIcon; }, 600); } function removeMarkBar() { if (markBar) { markBar.remove(); markBar = null; } if (markIcon) { markIcon.remove(); markIcon = null; } } function createMarkBar(scrollVal, barH) { removeMarkBar(); // 标记红线 markBar = document.createElement('div'); markBar.style.cssText = ` position:absolute;left:0;right:0;height:2px; background:#ff6b6b;box-shadow:0 0 4px #ff6b6b; z-index:10;pointer-events:none; `; markBar.style.top = (scrollVal + barH) + 'px'; txtBox.appendChild(markBar); // 书签图标 🔖 markIcon = document.createElement('span'); markIcon.innerText = '🔖'; markIcon.style.cssText = ` position:absolute;right:10px;font-size:16px; line-height:1;z-index:11;pointer-events:none; `; markIcon.style.top = (scrollVal + barH - 0) + 'px'; txtBox.appendChild(markIcon); } // 【核心】点击标记:当场读取当前标题栏真实高度并一起存储 function markPosition() { if (!titleBar) return; const currBarH = titleBar.offsetHeight; const currScroll = txtBox.scrollTop; markScrollTop = currScroll; markBarHeight = currBarH; localStorage.setItem('txtMarkPos', markScrollTop); localStorage.setItem('txtMarkBarH', markBarHeight); createMarkBar(currScroll, currBarH); } // 跳转:使用标记时保存的高度做偏移 function goMarkPosition() { txtBox.scrollTop = markScrollTop - markBarHeight; } function scrollToTop() { txtBox.scrollTop = 0; } // 显示/隐藏 标题栏+置顶按钮 function showHeaderAndTopBtn() { if (titleBar) { titleBar.style.transform = 'translateY(0)'; titleBar.style.opacity = '1'; titleBar.style.visibility = 'visible'; } if (topBtn) { topBtn.style.opacity = '1'; topBtn.style.visibility = 'visible'; } } function hideHeaderAndTopBtn() { if (titleBar) { titleBar.style.transform = 'translateY(-100%)'; titleBar.style.opacity = '0'; titleBar.style.visibility = 'hidden'; } if (topBtn) { topBtn.style.opacity = '0'; topBtn.style.visibility = 'hidden'; } } // 滚动逻辑 function onScroll() { const st = txtBox.scrollTop; const diff = st - lastScrollTop; if (diff < -5) { showHeaderAndTopBtn(); clearTimeout(scrollTimeout); } else if (diff > 5) { hideHeaderAndTopBtn(); clearTimeout(scrollTimeout); } lastScrollTop = st; } function initFileInput() { if (fileInput) return; fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.txt'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.onchange = function(e) { const file = e.target.files[0]; if (!file) return; currentFileName = file.name; const reader = new FileReader(); reader.onload = function(ev) { lastText = ev.target.result; localStorage.setItem('txtLastContent', lastText); updateContent(currentFileName, lastText); removeMarkBar(); }; reader.readAsText(file); fileInput.value = ''; }; } function saveAsFile() { const content = txtBox.textContent; if (!content) return; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = currentFileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function closeView() { if (mask) mask.remove(); mask = txtBox = wrapDom = markBar = markIcon = topBtn = titleBar = null; } function changeFontSize(type) { let sizeNum = parseFloat(currentFontSize); if (type === 'add') sizeNum += step; if (type === 'sub') sizeNum = Math.max(10, sizeNum - step); currentFontSize = sizeNum + 'px'; localStorage.setItem('txtFontSize', currentFontSize); txtBox.style.setProperty('font-size', currentFontSize, 'important'); } function toggleDarkMode() { isDarkMode = !isDarkMode; localStorage.setItem('txtDarkMode', isDarkMode); applyStyle(); } function changeBgColor() { bgIndex = (bgIndex + 1) % bgColorList.length; localStorage.setItem('txtBgIndex', bgIndex); applyStyle(); } function applyStyle() { if (isDarkMode) { wrapDom.style.setProperty('background-color', '#1a1a1a', 'important'); txtBox.style.setProperty('color', '#f0f0f0', 'important'); } else { wrapDom.style.setProperty('background-color', bgColorList[bgIndex], 'important'); txtBox.style.setProperty('color', '#000000', 'important'); } } function updateContent(fileName, content) { const titleText = mask.querySelector('.txt-title'); if(titleText) titleText.textContent = fileName; txtBox.textContent = content; } function createViewer(fileName, content) { currentFileName = fileName; mask = document.createElement('div'); mask.style.cssText = ` position:fixed;top:0;left:0;width:100vw;height:100vh; background:rgba(0,0,0,0.75);z-index:99999999; display:flex;justify-content:center;align-items:center; `; mask.onclick = (e) => { if (e.target === mask) closeView(); }; const wrap = document.createElement('div'); wrapDom = wrap; wrap.style.cssText = ` width:100vw;height:100vh;border-radius:0; display:flex;flex-direction:column;overflow:hidden; position:relative; `; titleBar = document.createElement('div'); // 标题栏半透明+毛玻璃 titleBar.style.cssText = ` padding:8px 10px; background:rgba(245,245,245,0.7) !important; backdrop-filter: blur(4px); border-bottom:1px solid rgba(238,238,238,0.4); display:flex; flex-direction:column; gap:8px; position:absolute; top:0; left:0; right:0; transition:all 0.3s ease; transform:translateY(0); opacity:1; visibility:visible; z-index:15; box-sizing:border-box; `; const titleText = document.createElement('span'); titleText.className = 'txt-title'; titleText.textContent = fileName; titleText.style.cssText = 'font-size:14px;word-wrap:break-word;'; const btnGroup = document.createElement('div'); // 单行+横向滚动 btnGroup.style.cssText = ` display:flex; align-items:center; flex-wrap:nowrap !important; gap:6px; width:100%; overflow-x:auto; overflow-y:hidden; padding-bottom:4px; `; // 放大按钮样式 const btnCommon = ` padding:6px 10px !important; border:1px solid #ccc !important; background:#fff !important; border-radius:6px !important; font-size:13px !important; white-space:nowrap !important; min-width:42px !important; height:32px !important; cursor:pointer !important; transition:all 0.2s ease !important; flex-shrink:0 !important; `; const selectFileBtn = document.createElement('button'); selectFileBtn.innerText = '📂'; selectFileBtn.title = '打开文件'; selectFileBtn.style.cssText = btnCommon; selectFileBtn.onclick = () => { showBtnTip(selectFileBtn, '打开', '📂'); initFileInput(); fileInput.click(); }; const saveBtn = document.createElement('button'); saveBtn.innerText = '💾'; saveBtn.title = '另存为'; saveBtn.style.cssText = btnCommon; saveBtn.onclick = () => { showBtnTip(saveBtn, '保存', '💾'); saveAsFile(); }; const minusBtn = document.createElement('button'); minusBtn.innerText = 'A-'; minusBtn.title = '缩小字号'; minusBtn.style.cssText = btnCommon; minusBtn.onclick = () => { showBtnTip(minusBtn, '缩小', 'A-'); changeFontSize('sub'); }; const plusBtn = document.createElement('button'); plusBtn.innerText = 'A+'; plusBtn.title = '放大字号'; plusBtn.style.cssText = btnCommon; plusBtn.onclick = () => { showBtnTip(plusBtn, '放大', 'A+'); changeFontSize('add'); }; const themeBtn = document.createElement('button'); themeBtn.innerText = isDarkMode ? '☀️' : '🌙'; themeBtn.title = '切换明暗主题'; themeBtn.style.cssText = btnCommon; themeBtn.onclick = () => { const icon = isDarkMode ? '☀️' : '🌙'; showBtnTip(themeBtn, '明暗', icon); toggleDarkMode(); themeBtn.innerText = isDarkMode ? '☀️' : '🌙'; }; const bgBtn = document.createElement('button'); bgBtn.innerText = '🎨'; bgBtn.title = '切换背景色'; bgBtn.style.cssText = btnCommon; bgBtn.onclick = () => { showBtnTip(bgBtn, '底色', '🎨'); changeBgColor(); }; const markBtn = document.createElement('button'); markBtn.innerText = '📌'; markBtn.title = '标记当前阅读位置'; markBtn.style.cssText = btnCommon; markBtn.onclick = () => { showBtnTip(markBtn, '标记', '📌'); markPosition(); }; const goMarkBtn = document.createElement('button'); goMarkBtn.innerText = '🚀'; goMarkBtn.title = '跳转到标记位置'; goMarkBtn.style.cssText = btnCommon; goMarkBtn.onclick = () => { showBtnTip(goMarkBtn, '跳转', '🚀'); goMarkPosition(); }; const closeBtn = document.createElement('button'); closeBtn.innerText = '✕'; closeBtn.title = '关闭窗口'; closeBtn.style.cssText = btnCommon; closeBtn.onclick = () => { showBtnTip(closeBtn, '关闭', '✕'); closeView(); }; btnGroup.appendChild(selectFileBtn); btnGroup.appendChild(saveBtn); btnGroup.appendChild(minusBtn); btnGroup.appendChild(plusBtn); btnGroup.appendChild(themeBtn); btnGroup.appendChild(bgBtn); btnGroup.appendChild(markBtn); btnGroup.appendChild(goMarkBtn); btnGroup.appendChild(closeBtn); titleBar.appendChild(titleText); titleBar.appendChild(btnGroup); txtBox = document.createElement('div'); txtBox.className = 'txt-content'; txtBox.style.cssText = ` flex:1;padding:16px;overflow:auto;white-space:pre-wrap; word-break:break-all;line-height:1.7;-webkit-overflow-scrolling:touch; position:relative; `; txtBox.style.setProperty('font-size', currentFontSize, 'important'); txtBox.textContent = content; topBtn = document.createElement('button'); topBtn.innerText = '⬆️'; topBtn.title = '回到顶部'; topBtn.style.cssText = ` position:absolute;right:20px;bottom:30px; width:44px;height:44px;border:none;border-radius:50%; background:#409eff;color:#fff;font-size:18px; box-shadow:0 2px 8px rgba(0,0,0,0.2); z-index:20;cursor:pointer; opacity:1;visibility:visible; transition:opacity 0.3s ease,visibility 0.3s ease; `; topBtn.onclick = () => { showBtnTip(topBtn, '顶部', '⬆️'); scrollToTop(); }; wrap.appendChild(titleBar); wrap.appendChild(txtBox); wrap.appendChild(topBtn); applyStyle(); mask.appendChild(wrap); document.body.appendChild(mask); setTimeout(() => { // 文本顶部留白跟随当前标题栏高度 const currH = titleBar.offsetHeight; txtBox.style.paddingTop = currH + 'px'; // 初始化:使用上次标记保存的高度+位置 if (markScrollTop > 0) { txtBox.scrollTop = markScrollTop - markBarHeight; createMarkBar(markScrollTop, markBarHeight); } txtBox.addEventListener('scroll', onScroll); }, 50); } window.openTxtFile = function() { if (lastText) createViewer('历史文本', lastText); else { initFileInput(); fileInput.click(); } }; window.clearTxtHistory = function() { localStorage.removeItem('txtLastContent'); localStorage.removeItem('txtMarkPos'); localStorage.removeItem('txtMarkBarH'); lastText = ''; markScrollTop = 0; markBarHeight = 0; }; })(); const otherItems = [ { text: ‘🔍 页面内查找’, action:()=>{menu.remove();showFindOverlay();} }, { text: ‘📝 编辑规则’, action:()=>{menu.remove();openReplaceDialog();} }, { text: ‘fontRow’, isFontRow: true, action:()=>{} }, { text: ‘🔄 刷新页面’, action:()=>{menu.remove();location.reload();} }, { text: ‘♻️ 重载规则(不刷新页面)’, action:()=>{menu.remove();forceReapplyAllRules();showToast(“已重载全部替换规则”,’success’);} }, { text: 🔧 规则管理 | 开启:${calcEnabledRuleTotal()}条 | 关闭:${replaceRules.length - calcEnabledRuleTotal()}条 | 总计:${replaceRules.length}条 | 本页生效:${activeRuleSet.size}条, dynamicText: true, action:()=>{menu.remove();openRuleManager();} }, { text: ‘📥 导入规则’, action:()=>{menu.remove();importRules();} }, { text: ‘📤 导出规则’, action:()=>{menu.remove();exportRules();} }, { text: ‘💾 保存当前文章’, action:()=>{menu.remove();saveCurrentArticle();} }, { text: ‘’, dataRefresh: true, action:()=>{menu.remove();restoreOriginalKeepRules();} }, { text: replaceRules.length > 0 ? 🗑️ 恢复原文(清空 ${replaceRules.length} 条规则) ‘🗑️🈳 已是原文(已清空规则)’, action:()=>{ menu.remove(); if (confirm(‘确定要恢复原文并清空所有规则吗?’)) { restoreOriginalClearRules(); } } }, //新增这一行悬浮图片 { text: ‘🖼️ 悬浮图片’, action:()=>{menu.remove();openFloatImgPanel();} }, // ===== 在这里新增 TXT 查看器菜单项 ===== { text: ‘📑 打开本地TXT’, action:()=>{menu.remove();openTxtFile();} }, // ====================================== { text: ‘❌ 关闭菜单’, action:()=>menu.remove() } ]; otherItems.forEach(item=>{ const div = document.createElement(‘div’); div.style.cssText = display:flex;align-items:center;padding:9px;margin:3px 0;background:#f9fafb;border-radius:8px;font-size:13px;color:#1f2937;cursor:pointer;; if (item.isFontRow) { div.style.cssText = “display:flex;gap:6px;padding:8px 4px;margin:3px 0;”; div.style.background = “transparent”; div.setAttribute(“isFontRow”, “1”); // 初始化按钮状态 div.style.opacity = fontAdjustSwitch ? "1" : "0.4"; div.style.pointerEvents = fontAdjustSwitch ? "auto" : "none"; // 开关按钮(始终可点击) const switchBtn = document.createElement("button"); switchBtn.textContent = fontAdjustSwitch ? "🔛 开启" : "🔚 关闭"; switchBtn.style.cssText = "padding:6px;border:none;border-radius:6px;background:#7c3aed;color:#fff;font-size:13px;cursor:pointer;"; switchBtn.style.pointerEvents = "auto"; switchBtn.onclick = function () { fontAdjustSwitch = !fontAdjustSwitch; GM_setValue('fontAdjustSwitch', fontAdjustSwitch); switchBtn.textContent = fontAdjustSwitch ? "🔛 开启" : "🔚 关闭"; div.style.opacity = fontAdjustSwitch ? "1" : "0.4"; div.style.pointerEvents = fontAdjustSwitch ? "auto" : "none"; if (fontAdjustSwitch) { // 数字拼接 px 再设置字体 setTextSize(textSize + 'px'); showToast("字体调节已启用"); } else { // 关闭时实时读取页面原生字体,精准还原 let realOriginFont = getComputedStyle(document.body).fontSize; setTextSize(realOriginFont); showToast("字体调节已禁用,字体已还原"); } }; // 通用清除计时器 function clearTimers(t1, t2) { clearTimeout(t1); clearInterval(t2); } // 字体减按钮 const btnMin = document.createElement("button"); btnMin.textContent = "字体-"; btnMin.style.cssText = "flex:1;padding:6px;border:none;border-radius:6px;background:#6b7280;color:#fff;font-size:13px;cursor:pointer;"; let minTimer = null; let minLoop = null; let minIsLong = false; function minStart() { minIsLong = false; minTimer = setTimeout(() => { minIsLong = true; minLoop = setInterval(fontShrink, REPEAT_INTERVAL); }, LONG_DELAY); } function minEnd() { clearTimers(minTimer, minLoop); if (!minIsLong) fontShrink(); localStorage.setItem('textSize', textSize); minIsLong = false; } btnMin.addEventListener('mousedown', minStart); btnMin.addEventListener('touchstart', minStart); btnMin.addEventListener('mouseup', minEnd); btnMin.addEventListener('mouseleave', minEnd); btnMin.addEventListener('touchend', minEnd); btnMin.addEventListener('touchcancel', minEnd); // 字体加按钮 const btnPlus = document.createElement("button"); btnPlus.textContent = "字体+"; btnPlus.style.cssText = btnMin.style.cssText; btnPlus.style.background = "#3b82f6"; let plusTimer = null; let plusLoop = null; let plusIsLong = false; function plusStart() { plusIsLong = false; plusTimer = setTimeout(() => { plusIsLong = true; plusLoop = setInterval(fontEnlarge, REPEAT_INTERVAL); }, LONG_DELAY); } function plusEnd() { clearTimers(plusTimer, plusLoop); if (!plusIsLong) fontEnlarge(); localStorage.setItem('textSize', textSize); plusIsLong = false; } btnPlus.addEventListener('mousedown', plusStart); btnPlus.addEventListener('touchstart', plusStart); btnPlus.addEventListener('mouseup', plusEnd); btnPlus.addEventListener('mouseleave', plusEnd); btnPlus.addEventListener('touchend', plusEnd); btnPlus.addEventListener('touchcancel', plusEnd); // 重置按钮 const btnReset = document.createElement("button"); btnReset.textContent = "重置"; btnReset.style.cssText = btnMin.style.cssText; btnReset.style.background = "#10b981"; let resetTimer = null; let resetIsLong = false; function resetStart() { resetIsLong = false; resetTimer = setTimeout(() => { resetIsLong = true; let inputVal = prompt("请输入字体大小(px)\n范围:10 ~ 99", textSize); if (inputVal !== null) { let num = parseInt(inputVal); if (!isNaN(num)) { setTextSize(num); localStorage.setItem('textSize', num); } } }, LONG_DELAY); } function resetEnd() { clearTimeout(resetTimer); if (!resetIsLong) { fontReset(); localStorage.setItem('textSize', textSize); } resetIsLong = false; } btnReset.addEventListener('mousedown', resetStart); btnReset.addEventListener('touchstart', resetStart); btnReset.addEventListener('mouseup', resetEnd); btnReset.addEventListener('mouseleave', resetEnd); btnReset.addEventListener('touchend', resetEnd); btnReset.addEventListener('touchcancel', resetEnd); // 排列顺序:开关 → 字体- → 字体+ → 重置 div.appendChild(switchBtn); div.appendChild(btnMin); div.appendChild(btnPlus); div.appendChild(btnReset); content.appendChild(div); return; } // 原有普通菜单项逻辑不变 if (item.dataRefresh) { div.setAttribute('data-refresh', 'true'); } else if (item.dynamicText) { div.setAttribute('dynamicText','true'); div.textContent = item.text; } else { div.textContent = item.text; } div.onclick = item.action; content.appendChild(div); }); function refreshMenuText() { // 刷新【恢复原文】行 const menuItem = content.querySelector(‘[data-refresh]’); if (menuItem) { const allRuleNum = replaceRules.length; if (isReplacing) { menuItem.textContent = 📖 已替换 ${totalReplacedCount} 条,单击恢复原文、保留规则; } else { menuItem.textContent = 📄 已恢复原文(保留 ${allRuleNum} 条规则); } } // 规则统计实时刷新 const ruleLine = content.querySelector(‘[dynamicText]’); if (ruleLine) { ruleLine.textContent = 🔧 规则管理 | 开启:${calcEnabledRuleTotal()}条 | 关闭:${replaceRules.length - calcEnabledRuleTotal()}条 | 总计:${replaceRules.length}条 | 本页生效:${activeRuleSet.size}条; } } // 每200毫秒自动执行刷新 calcEnabledRuleTotal(); refreshMenuText(); const refreshTimer = setInterval(refreshMenuText, 200); menu.addEventListener(‘remove’, () => clearInterval(refreshTimer)); const closeArea = document.createElement(‘div’); closeArea.style.cssText = height:45px;display:flex;align-items:center;justify-content:center;color:#888;font-size:11px;; closeArea.textContent = ‘点击空白关闭’; menu.appendChild(content);menu.appendChild(closeArea); menu.addEventListener(‘click’,e=>{if(e.target===menu)menu.remove();}); document.body.appendChild(menu); } function openRuleManager() { let oldRulePanel = document.querySelector(‘.tm-rule-manager’); if(oldRulePanel) oldRulePanel.remove(); const existingMask = document.querySelector(‘.tm-rule-manager’); if (existingMask) existing.remove(); const mask = document.createElement(“div”); mask.className = “tm-rule-manager”; mask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:2147483644;display:flex;align-items:center;justify-content:center;padding:15px;; const box = document.createElement(“div”); box.style.cssText = width:82%;max-width:320px;max-height:60vh;background:#fff;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,0.2);display:flex;flex-direction:column;overflow:hidden;; const header = document.createElement(“div”); header.style.cssText = background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:12px 15px;font-size:15px;font-weight:bold;text-align:center;; header.textContent = “规则管理”; const list = document.createElement(“div”); list.style.cssText = flex:1;overflow-y:auto;padding:10px;max-height:42vh;; if (replaceRules.length === 0) { const empty = document.createElement(“div”); empty.style.cssText = text-align:center;padding:20px 15px;color:#9ca3af;font-size:13px;; empty.textContent = “暂无规则”; list.appendChild(empty); } else { replaceRules.forEach((rule, index) => { const item = document.createElement(“div”); item.style.cssText = padding:8px;margin:4px 0;background:#f9fafb;border-radius:6px;border:1px solid #e5e7eb;; const fromText = rule.from.length > 22 ? rule.from.substring(0,22)+”…” : rule.from; const toText = rule.to.length > 12 ? rule.to.substring(0,12)+”…” : rule.to; item.innerHTML = ` "${fromText}" → "${toText}" ${isRegexRule(rule.from)?'正则':'普通'} • ${rule.enabled?'✅启用':'❌禁用'} 编辑 ${rule.enabled?'禁用':'启用'} 删除 `; item.querySelector(‘.edit-btn’).addEventListener(‘click’,()=>{ mask.remove(); openEditRuleDialog(index); }); item.querySelector(‘.toggle-btn’).addEventListener(‘click’, () => { replaceRules[index].enabled = !replaceRules[index].enabled; GM_setValue(‘saveReplaceRules’, replaceRules); isReplacing && forceReapplyAllRules(); openRuleManager(); }); item.querySelector(‘.delete-btn’).addEventListener(‘click’, () => { if(confirm(‘确定删除这条规则?’)){ replaceRules.splice(index,1); GM_setValue(‘saveReplaceRules’,replaceRules); resetActiveRuleSet(); isReplacing && forceReapplyAllRules(); // 删掉openRuleManager,改成下面两行 document.querySelector(‘.tm-rule-manager’)?.remove(); openRuleManager(); } }) list.appendChild(item); }); } const footer = document.createElement(“div”); footer.style.cssText = background:#fff;padding:10px;border-top:1px solid #e5e7eb;display:flex;gap:8px;; const newBtn = document.createElement(“button”); newBtn.textContent = “➕ 新规则”; newBtn.style.cssText = flex:1;padding:8px;border-radius:6px;border:none;background:#3b82f6;color:#fff;font-size:13px;; newBtn.onclick = () => {mask.remove();openReplaceDialog();}; const closeBtn = document.createElement(“button”); closeBtn.textContent = “关闭”; closeBtn.style.cssText = flex:1;padding:8px;border:1px solid #d1d5db;background:#fff;color:#374151;font-size:13px;; closeBtn.onclick = () => mask.remove(); footer.appendChild(newBtn);footer.appendChild(closeBtn); box.appendChild(header);box.appendChild(list);box.appendChild(footer); mask.appendChild(box);document.body.appendChild(mask); mask.onclick = e => e.target === mask && mask.remove(); } function openEditRuleDialog(ruleIndex) { const targetRule = replaceRules[ruleIndex]; if(!targetRule) return; const existingMask = document.querySelector(‘.tm-replace-dialog-mask’); if (existingMask) existing.remove(); const mask = document.createElement(“div”); mask.className = “tm-replace-dialog-mask”; mask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483644;display:flex;align-items:center;justify-content:center;padding:15px;; const box = document.createElement(“div”); box.style.cssText = width:82%;max-width:320px;background:#fff;padding:15px;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,0.2);; const title = document.createElement(“div”); title.style.cssText = “font-size:16px;font-weight:bold;margin-bottom:12px;color:#1f2937;text-align:center;”; title.textContent = “修改替换规则”; const form = document.createElement(“div”); form.style.cssText = “display:flex;flex-direction:column;gap:10px;”; const fromGroup = document.createElement(“div”); fromGroup.innerHTML = ` 被替换内容 普通文本 或 /测试\d+/g ; const toGroup = document.createElement("div"); toGroup.innerHTML = 替换为 ; const tipLine = document.createElement("div"); tipLine.style.cssText = "font-size:11px;color:#999;text-align:center;margin:4px 0;"; tipLine.innerText = "多行分行填写可批量添加对应规则"; const symbols = document.createElement("div"); symbols.style.cssText = "display:flex;gap:4px;flex-wrap:wrap;margin:6px 0;justify-content:center;"; const symbolList = ['/', '\\', '|', '(', ')', '[', ']', '{', '}', '*', '+', '?', '.', '=', ',']; symbolList.forEach(sym => { const btn = document.createElement("button"); btn.textContent = sym; btn.style.cssText = width:28px;height:28px;border:none;border-radius:4px;background:#f3f4f6;color:#1f2937;font-size:12px;font-weight:bold;cursor:pointer;; btn.onclick = () => { const input = document.activeElement.tagName==='TEXTAREA'?document.activeElement:document.getElementById("tmEditFrom"); const start = input.selectionStart; const end = input.selectionEnd; input.value = input.value.substring(0,start)+sym+input.value.substring(end); input.focus(); input.selectionStart = input.selectionEnd = start+1; }; symbols.appendChild(btn); }); const controls = document.createElement("div"); controls.style.cssText = "display:flex;gap:6px;margin-top:12px;"; const saveBtn = document.createElement("button"); saveBtn.textContent = "保存修改"; saveBtn.style.cssText = flex:1;padding:9px;border:none;border-radius:8px;background:#10b981;color:#fff;font-size:12px;font-weight:bold;; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "取消"; cancelBtn.style.cssText = flex:1;padding:9px;border:2px solid #e5e7eb;border-radius:8px;background:#fff;color:#374151;font-size:12px;; controls.appendChild(saveBtn); controls.appendChild(cancelBtn); form.appendChild(fromGroup); form.appendChild(toGroup); form.appendChild(tipLine); form.appendChild(symbols); form.appendChild(controls); box.appendChild(title); box.appendChild(form); mask.appendChild(box); document.body.appendChild(mask); setTimeout(() => { const fromTa = document.getElementById("tmEditFrom"); const toTa = document.getElementById("tmEditTo"); if (fromTa) fromTa.value = targetRule.from; if (toTa) toTa.value = targetRule.to; fromTa?.focus(); }, 50); saveBtn.onclick = () => { const newFrom = document.getElementById("tmEditFrom").value.trim(); const newTo = document.getElementById("tmEditTo").value.trim(); if(!newFrom){ showToast("替换内容不能为空", "error"); return; } replaceRules[ruleIndex].from = newFrom; replaceRules[ruleIndex].to = newTo; updateRulesAndApply([...replaceRules]); mask.remove(); showToast("规则修改成功", "success"); }; cancelBtn.onclick = () => mask.remove(); mask.onclick = e => e.target === mask && mask.remove(); } function openReplaceDialog() { const existing = document.querySelector('.tm-replace-dialog-mask'); if (existing) existing.remove(); const mask = document.createElement('div'); mask.className = 'tm-replace-dialog-mask'; mask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483644;display:flex;align-items:center;justify-content:center;padding:15px;; const box = document.createElement('div'); box.style.cssText = width:85%;max-width:340px;background:#fff;padding:15px;border-radius:14px;; const title = document.createElement('div'); title.style.cssText = font-size:16px;font-weight:bold;text-align:center;margin-bottom:12px;; title.textContent = '新增替换规则'; const fromTa = document.createElement('textarea'); fromTa.id = 'tmAddFrom'; fromTa.placeholder = '被替换内容(多行批量)\n正则示例:/匹配.+文本/g'; fromTa.style.cssText = width:100%;height:70px;padding:8px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical;box-sizing:border-box;; const toTa = document.createElement('textarea'); toTa.id = 'tmAddTo'; toTa.placeholder = '替换为(对应行数)'; toTa.style.cssText = width:100%;height:70px;padding:8px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical;box-sizing:border-box;margin-top:10px;; const tips = document.createElement('div'); tips.style.cssText = font-size:11px;color:#999;text-align:center;margin:6px 0;; tips.textContent = '一行一条,行数必须一致;支持正则 /xxx/g'; const symbols = document.createElement('div'); symbols.style.cssText = display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin:8px 0;; const syms = ['/', '\\', '|', '(', ')', '[', ']', '{', '}', '*', '+', '?', '.', '=', ',']; syms.forEach(sym => { const btn = document.createElement('button'); btn.textContent = sym; btn.style.cssText = width:28px;height:28px;border:none;border-radius:4px;background:#f3f4f6;font-size:12px;; btn.onclick = () => { const target = document.activeElement.tagName === 'TEXTAREA' ? document.activeElement : fromTa; const start = target.selectionStart; const end = target.selectionEnd; target.value = target.value.slice(0, start) + sym + target.value.slice(end); target.focus(); target.selectionStart = target.selectionEnd = start + 1; }; symbols.appendChild(btn); }); const btnWrap = document.createElement('div'); btnWrap.style.cssText = display:flex;gap:8px;margin-top:12px;; const addBtn = document.createElement('button'); addBtn.textContent = '添加规则'; addBtn.style.cssText = flex:1;padding:9px;border:none;border-radius:8px;background:#10b981;color:#fff;; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = flex:1;padding:9px;border:2px solid #e5e7eb;border-radius:8px;background:#fff;color:#333;; addBtn.onclick = () => { const fromVal = fromTa.value; const toVal = toTa.value; addMultipleRules(fromVal, toVal); mask.remove(); }; cancelBtn.onclick = () => mask.remove(); btnWrap.appendChild(addBtn); btnWrap.appendChild(cancelBtn); box.appendChild(title); box.appendChild(fromTa); box.appendChild(toTa); box.appendChild(tips); box.appendChild(symbols); box.appendChild(btnWrap); mask.appendChild(box); document.body.appendChild(mask); setTimeout(()=>fromTa.focus(),50); mask.onclick = e => e.target === mask && mask.remove(); } // 规则导入导出 function exportRules() { // 双模式选择弹窗 const mask = document.createElement("div"); mask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:2147483650;display:flex;align-items:center;justify-content:center;padding:15px;; const box = document.createElement("div"); box.style.cssText = width:80%;max-width:300px;background:#fff;border-radius:12px;overflow:hidden;; const titleBar = document.createElement("div"); titleBar.style.cssText = padding:14px;text-align:center;font-size:15px;font-weight:bold;color:#222;border-bottom:1px solid #eee;; titleBar.textContent = "选择导出方式"; const btnWrap = document.createElement("div"); btnWrap.style.cssText = padding:12px;display:flex;gap:10px;; const downloadBtn = document.createElement("button"); downloadBtn.style.cssText = flex:1;padding:10px;border:none;border-radius:8px;background:#3b82f6;color:#fff;font-size:13px;; downloadBtn.textContent = "下载JSON文件"; const copyBtn = document.createElement("button"); copyBtn.style.cssText = flex:1;padding:10px;border:none;border-radius:8px;background:#10b981;color:#fff;font-size:13px;; copyBtn.textContent = "复制规则文本"; const closeBtn = document.createElement("button"); closeBtn.style.cssText = width:100%;padding:9px;border:none;border-top:1px solid #eee;background:#f8f8f8;color:#666;font-size:13px;; closeBtn.textContent = "取消"; btnWrap.appendChild(downloadBtn); btnWrap.appendChild(copyBtn); box.appendChild(titleBar); box.appendChild(btnWrap); box.appendChild(closeBtn); mask.appendChild(box); document.body.appendChild(mask); const jsonStr = JSON.stringify(replaceRules, null, 2); downloadBtn.onclick = () => { const blob = new Blob([jsonStr], {type:"application/json;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "文本替换规则.json"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); mask.remove(); showToast("规则文件下载成功", "success"); }; copyBtn.onclick = async () => { try { await navigator.clipboard.writeText(jsonStr); mask.remove(); showToast("内容已复制", "success"); const editMask = document.createElement("div"); editMask.style.cssText = position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483652;display:flex;align-items:center;justify-content:center;padding:15px;; const editBox = document.createElement("div"); editBox.style.cssText = width:90%;max-width:350px;background:#fff;border-radius:12px;padding:15px;; const editTitle = document.createElement("div"); editTitle.style.cssText = font-size:15px;font-weight:500;margin-bottom:10px;text-align:center;; editTitle.textContent = "编辑规则内容"; const textArea = document.createElement("textarea"); textArea.style.cssText = width:100%; height:200px; padding:8px; border:1px solid #e5e7eb; border-radius:8px; box-sizing:border-box; font-size:13px; overflow-y:auto; overflow-x:hidden; resize:none; ; textArea.value = jsonStr; textArea.placeholder = "可直接修改规则配置"; // ===== 新增翻页按钮 ===== const navBtnWrap = document.createElement("div"); navBtnWrap.style.cssText = "display:flex;gap:6px;margin:10px 0;"; const makeNavBtn = (txt) => { let b = document.createElement("button"); b.style.cssText = "flex:1;padding:6px;border-radius:5px;border:none;background:#eef1f5;color:#222;font-size:12px;"; b.textContent = txt; return b; }; const btnTop = makeNavBtn("顶部"); const btnUp = makeNavBtn("上翻"); const btnDown = makeNavBtn("下翻"); const btnBottom = makeNavBtn("底部"); navBtnWrap.append(btnTop, btnUp, btnDown, btnBottom); btnTop.onclick = () => textArea.scrollTop = 0; btnBottom.onclick = () => textArea.scrollTop = textArea.scrollHeight; btnUp.onclick = () => textArea.scrollTop -= textArea.clientHeight * 0.8; btnDown.onclick = () => textArea.scrollTop += textArea.clientHeight * 0.8; // ====================== const btnRow = document.createElement("div"); btnRow.style.cssText = display:flex;gap:8px;margin-top:5px;; const saveLocalBtn = document.createElement("button"); saveLocalBtn.style.cssText = flex:1;padding:8px;border:none;border-radius:6px;background:#10b981;color:#fff;; saveLocalBtn.textContent = "重新保存文件"; const exitBtn = document.createElement("button"); exitBtn.style.cssText = flex:1;padding:8px;border:1px solid #ddd;border-radius:6px;background:#fff;`; exitBtn.textContent = “完成关闭”; btnRow.appendChild(saveLocalBtn); btnRow.appendChild(exitBtn); // 顺序:标题 → 文本框 → 翻页按钮 → 操作按钮 editBox.appendChild(editTitle); editBox.appendChild(textArea); editBox.appendChild(navBtnWrap); editBox.appendChild(btnRow); editMask.appendChild(editBox); document.body.appendChild(editMask); textArea.focus(); saveLocalBtn.onclick = function() { const newTxt = textArea.value; const parentBox = this.closest('[style*="z-index"]'); if (!parentBox) return; let dropBox = parentBox.querySelector('.inner-save-dropdown'); if (dropBox) { dropBox.remove(); return; } dropBox = document.createElement('div'); dropBox.className = 'inner-save-dropdown'; dropBox.style.cssText = ` margin-top:6px; border-radius:8px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.12); background:#fff; `; const itemBase = ` display:block; width:100%; padding:9px 12px; border:none; background:transparent; font-size:13px; text-align:center; cursor:pointer; color:#333; transition:background 0.2s; `; const btnJson = document.createElement('button'); btnJson.style.cssText = itemBase; btnJson.textContent = 'JSON 规则文件'; btnJson.onmouseover = () => btnJson.style.background = '#f5f7fa'; btnJson.onmouseout = () => btnJson.style.background = 'transparent'; const btnTxt = document.createElement('button'); btnTxt.style.cssText = itemBase + 'border-top:1px solid #f0f0f0;'; btnTxt.textContent = 'TXT 纯文本'; btnTxt.onmouseover = () => btnTxt.style.background = '#f5f7fa'; btnTxt.onmouseout = () => btnTxt.style.background = 'transparent'; const btnMd = document.createElement('button'); btnMd.style.cssText = itemBase + 'border-top:1px solid #f0f0f0;'; btnMd.textContent = 'Markdown 文档'; btnMd.onmouseover = () => btnMd.style.background = '#f5f7fa'; btnMd.onmouseout = () => btnMd.style.background = 'transparent'; dropBox.append(btnJson, btnTxt, btnMd); this.after(dropBox); // JSON保存 btnJson.onclick = () => { dropBox.remove(); const blob = new Blob([newTxt], {type:"application/json;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "文本替换规则_编辑版.json"; a.click(); a.remove(); URL.revokeObjectURL(url); showToast("JSON 规则保存成功", "success"); }; // TXT保存 btnTxt.onclick = () => { dropBox.remove(); const blob = new Blob([newTxt], {type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "文本替换规则_编辑版.txt"; a.click(); a.remove(); URL.revokeObjectURL(url); showToast("TXT 文件保存成功", "success"); }; // MD保存 btnMd.onclick = () => { dropBox.remove(); const blob = new Blob([newTxt], {type:"text/markdown;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "文本替换规则_编辑版.md"; a.click(); a.remove(); URL.revokeObjectURL(url); showToast("Markdown 文件保存成功", "success"); }; }; exitBtn.onclick = () => editMask.remove(); editMask.onclick = e => e.target === editMask && editMask.remove(); } catch (err) { const textarea = document.createElement('textarea'); textarea.value = jsonStr; textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); textarea.remove(); mask.remove(); showToast("规则文本已复制", "success"); } }; closeBtn.onclick = () => mask.remove(); mask.onclick = e => { if(e.target === mask) mask.remove(); }; } function importRules() { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = e => { const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = res => { try { const arr = JSON.parse(res.target.result); if(!Array.isArray(arr)) throw new Error(); replaceRules = arr; GM_setValue("saveReplaceRules", replaceRules); isReplacing && forceReapplyAllRules(); showToast("规则导入成功", "success"); } catch { showToast("规则文件格式错误", "error"); } }; reader.readAsText(file); }; input.click(); } // 统计本页实际生效规则(最终版) function getPageActiveCount() { return activeRuleSet.size; } // 悬浮图片↓ let floatImgs = []; // 记录面板上次位置 let panelLastLeft = 10; let panelLastTop = 160; // 撤销 + 重做 历史栈 let imgHistory = []; let imgRedoStack = []; const HISTORY_MAX = 20; let batchSelectMode = false; let batchAdjustMode = false; let selectedImgs = new Set(); const PANEL_Z = 9999; const IMG_BASE = PANEL_Z + 1; // 裁剪全局 let cropBox = null; let cropImg = null; let cropIsDrag = false; let resizeDir = ‘’; let startW = 0, startH = 0; let startL = 0, startT = 0; let startX = 0, startY = 0; let lockRatio = 0; // 0自由 1=1:1 2=4:3 3=16:9 // 保存状态到撤销栈,清空重做栈(新操作后不能再重做) function saveImgState() { const state = floatImgs.map(item => ({ left: item.dom.style.left, top: item.dom.style.top, width: item.dom.style.width, height: item.dom.style.height, zIndex: item.zIndex, display: item.dom.style.display, opacity: item.opacity, penetrate: item.penetrate, imgSrc: item.imgSrc || “” })); imgHistory.push(state); // 新操作,清空重做记录 imgRedoStack = []; if (imgHistory.length > HISTORY_MAX) { imgHistory.shift(); } } // 撤销 function undoImgOperate() { if (imgHistory.length <= 1) { alert(“已经是最早状态,无法撤销”); return; } // 当前状态压入重做栈 const current = imgHistory.pop(); imgRedoStack.push(current); const lastState = imgHistory[imgHistory.length - 1]; restoreState(lastState); refreshList(); } // 重做 function redoImgOperate() { if (imgRedoStack.length === 0) { alert(“暂无可重做操作”); return; } const redoState = imgRedoStack.pop(); imgHistory.push(redoState); restoreState(redoState); refreshList(); } // 统一恢复状态方法【修复:补全图片+轮廓+选中状态】 function restoreState(state) { // 先清空现有图片与选中 floatImgs.forEach(item => item.dom.remove()); floatImgs = []; selectedImgs.clear(); // 根据历史状态重建图片 state.forEach((s, idx) => { const container = document.createElement(‘div’); container.style.cssText = ` position:fixed;z-index:${s.zIndex};background:#fff; overflow:hidden;opacity:${s.opacity}; left:${s.left};top:${s.top}; width:${s.width};height:${s.height}; pointer-events:${s.penetrate ? “none” : “auto”}; box-sizing:border-box; display:${s.display}; `; const openBtn = document.createElement('div'); openBtn.dataset.noDrag = 'true'; openBtn.style.cssText = ` position:absolute;top:0;left:0;width:24px;height:24px; background:transparent;cursor:pointer; z-index:9999;pointer-events:auto !important; `; openBtn.onclick = openBtn.ontouchstart = e => { e.stopPropagation(); openFloatImgPanel(); }; const closeBtn = document.createElement('div'); closeBtn.dataset.noDrag = 'true'; closeBtn.style.cssText = ` position:absolute;top:0;right:0;width:24px;height:24px; background:transparent;cursor:pointer; z-index:9999;pointer-events:auto !important; `; closeBtn.onclick = closeBtn.ontouchstart = e => { e.stopPropagation(); container.remove(); floatImgs = floatImgs.filter(i => i.dom !== container); }; const img = document.createElement('img'); img.style.cssText = `width:100%;height:100%;display:block;pointer-events:none;`; // 核心修复:强制还原图片地址 if (s.imgSrc) { img.src = s.imgSrc; } container.append(openBtn, closeBtn, img); document.body.appendChild(container); const imgItem = { dom: container, penetrate: s.penetrate, opacity: s.opacity, zIndex: s.zIndex, imgSrc: s.imgSrc }; floatImgs.push(imgItem); // 绑定事件 container.onmousedown = (e) => { if (e.target.closest('[data-noDrag]')) return; if (!batchSelectMode) { openFloatImgPanel(); return; } e.preventDefault(); toggleSelect(floatImgs.indexOf(imgItem)); }; touchDrag(container, container, floatImgs.indexOf(imgItem)); pinchZoom(container, floatImgs.indexOf(imgItem)); }); } function openFloatImgPanel() { if (document.querySelector(‘.img-set-panel’)) return; let setDiv = document.createElement(‘div’); setDiv.className = ‘img-set-panel’; setDiv.style.cssText = ` position:fixed;z-index:9000; top:${panelLastTop}px;left:${panelLastLeft}px; background:#fff;padding:6px; border:1px solid #ccc;border-radius:6px; display:flex;flex-direction:column;gap:4px; min-width:190px;max-width:210px; font-size:12px;pointer-events:auto; box-shadow:0 2px 8px #0003; `; // 左右分栏布局 setDiv.innerHTML = ` 悬浮图片 已选 0 张 透明度: 图片穿透 添加图片 撤销 重做 隐藏选中 批量选择 顶 底 上 下 删除图片 全选 清空 裁剪保存 关闭面板 `; // 绑定撤销、重做按钮 setDiv.querySelector(‘#undoBtn’).onclick = undoImgOperate; setDiv.querySelector(‘#redoBtn’).onclick = redoImgOperate; document.body.appendChild(setDiv); const dragBar = setDiv.querySelector(‘#dragBar’); touchDrag(dragBar, setDiv, -1); // 拖动结束保存位置 const savePos = () => { panelLastLeft = parseInt(setDiv.style.left) || 10; panelLastTop = parseInt(setDiv.style.top) || 160; }; document.addEventListener(‘mouseup’, savePos); document.addEventListener(‘touchend’, savePos); // 关闭面板时移除监听,防止重复绑定 setDiv.querySelector(‘#closePanel’).addEventListener(‘click’, () => { document.removeEventListener(‘mouseup’, savePos); document.removeEventListener(‘touchend’, savePos); setDiv.remove(); }); setDiv.querySelectorAll(‘button’).forEach(btn => { btn.style.cssText = ` padding:2px 4px;font-size:11px;border-radius:4px; border:1px solid #ccc;background:#f7f7f7; `; }); const list = setDiv.querySelector(‘#imgList’); const slider = setDiv.querySelector(‘#imgOpaSlider’); function refreshList() { list.innerHTML = ‘’; floatImgs.forEach((img, i) => { const div = document.createElement(‘div’); div.textContent = 图${i + 1}(${img.zIndex}); div.style.cssText = ` padding:2px 4px;cursor:pointer; border:1px solid ${selectedImgs.has(i) ? ‘#f00’ : ‘#ccc’}; display:${img.dom.style.display === ‘none’ ? ‘none’ : ‘block’}; ; div.onclick = () => toggleSelect(i); list.appendChild(div); }); setDiv.querySelector('#selectedInfo').textContent = 已选 ${selectedImgs.size} 张`; const selectAllBtn = setDiv.querySelector(‘#selectAll’); selectAllBtn.textContent = (floatImgs.length && floatImgs.length === selectedImgs.size) ? ‘取消全选’ : ‘全选’; slider.disabled = batchAdjustMode ? false : selectedImgs.size !== 1; if (!batchAdjustMode && selectedImgs.size === 1) { slider.value = floatImgs[[…selectedImgs][0]].opacity * 100; } } function toggleSelect(i) { selectedImgs.has(i) ? (selectedImgs.delete(i), floatImgs[i].dom.style.outline = ‘none’) : (selectedImgs.add(i), floatImgs[i].dom.style.outline = ‘2px solid red’); refreshList(); } setDiv.querySelector(‘#closePanel’).onclick = () => setDiv.remove(); // 删除图片 setDiv.querySelector(‘#closeImg’).onclick = () => { if(selectedImgs.size === 0) return; saveImgState(); selectedImgs.forEach(i => floatImgs[i].dom.remove()); floatImgs = floatImgs.filter((_, i) => !selectedImgs.has(i)); selectedImgs.clear(); refreshList(); }; setDiv.querySelector(‘#batchSelect’).onclick = () => { batchSelectMode = !batchSelectMode; batchAdjustMode = batchSelectMode; if (batchSelectMode) { selectedImgs.clear(); floatImgs.forEach((_, i) => selectedImgs.add(i)); floatImgs.forEach(i => i.dom.style.outline = ‘2px solid red’); } else { selectedImgs.clear(); floatImgs.forEach(i => i.dom.style.outline = ‘none’); } setDiv.querySelector(‘#batchSelect’).textContent = batchSelectMode ? ‘退出批量’ : ‘批量选择’; refreshList(); }; // 全选 setDiv.querySelector(‘#selectAll’).onclick = () => { saveImgState(); const all = floatImgs.length && floatImgs.length === selectedImgs.size; all ? selectedImgs.clear() : floatImgs.forEach((_, i) => selectedImgs.add(i)); floatImgs.forEach(i => i.dom.style.outline = all ? ‘none’ : ‘2px solid red’); refreshList(); }; // 清空选择 setDiv.querySelector(‘#clearSelect’).onclick = () => { selectedImgs.clear(); floatImgs.forEach(i => i.dom.style.outline = ‘none’); refreshList(); }; // 图片穿透 setDiv.querySelector(‘#penetrateCheck’).onchange = e => { if(selectedImgs.size === 0) return; saveImgState(); selectedImgs.forEach(i => { floatImgs[i].penetrate = e.target.checked; floatImgs[i].dom.style.pointerEvents = e.target.checked ? ‘none’ : ‘auto’; }); }; // 添加图片 setDiv.querySelector(‘#makeFloatImg’).onclick = () => { const url = setDiv.querySelector(‘#imgNetUrl’).value.trim(); const files = setDiv.querySelector(‘#imgLocalFile’).files; const opa = parseFloat(slider.value) / 100 || 1; if (files.length > 0) Array.from(files).forEach(file => createFloatImage(null, file, opa)); else if (url) createFloatImage(url, null, opa); refreshList(); }; // 显示/隐藏选中 setDiv.querySelector(‘#toggleShowHide’).onclick = () => { if (selectedImgs.size === 0) return; saveImgState(); const hide = !Array.from(selectedImgs).some(i => floatImgs[i].dom.style.display === ‘none’); selectedImgs.forEach(i => floatImgs[i].dom.style.display = hide ? ‘none’ : ‘block’); setDiv.querySelector(‘#toggleShowHide’).textContent = hide ? ‘显示选中’ : ‘隐藏选中’; refreshList(); }; // 裁剪 setDiv.querySelector(‘#cropImgBtn’).onclick = () => { saveImgState(); if (selectedImgs.size !== 1) { alert(‘请只选择一张图片进行裁剪’); return; } const idx = […selectedImgs][0]; const currentOpacity = floatImgs[idx].opacity; openCropPanel(floatImgs[idx].dom.querySelector(‘img’), currentOpacity); }; function getVisualOrder() { return floatImgs.map((img, i) => ({ i, z: img.zIndex })).sort((a, b) => b.z - a.z).map(v => v.i); } // 置顶 setDiv.querySelector(‘#toTop’).onclick = () => { if (selectedImgs.size === 0) return; saveImgState(); const maxZ = Math.max(…floatImgs.map(img => img.zIndex)); selectedImgs.forEach(i => { floatImgs[i].zIndex = maxZ + 10; floatImgs[i].dom.style.zIndex = floatImgs[i].zIndex; }); refreshList(); }; // 置底 setDiv.querySelector(‘#toBottom’).onclick = () => { if (selectedImgs.size === 0) return; saveImgState(); const others = floatImgs.filter((_, i) => !selectedImgs.has(i)); const minZ = others.length ? Math.min(…others.map(img => img.zIndex)) : IMG_BASE; const bottomZ = minZ - 10; selectedImgs.forEach(i => { floatImgs[i].zIndex = bottomZ; floatImgs[i].dom.style.zIndex = bottomZ; }); refreshList(); }; // 上移层级 setDiv.querySelector(‘#moveUp’).onclick = () => { if (selectedImgs.size === 0) return; saveImgState(); const order = getVisualOrder(); for (let i = 1; i < order.length; i++) { const cur = order[i]; const prev = order[i - 1]; if (selectedImgs.has(cur) && !selectedImgs.has(prev)) { [floatImgs[cur].zIndex, floatImgs[prev].zIndex] = [floatImgs[prev].zIndex, floatImgs[cur].zIndex]; floatImgs[cur].dom.style.zIndex = floatImgs[cur].zIndex; floatImgs[prev].dom.style.zIndex = floatImgs[prev].zIndex; } } refreshList(); }; // 下移层级 setDiv.querySelector(‘#moveDown’).onclick = () => { if (selectedImgs.size === 0) return; saveImgState(); const order = getVisualOrder(); for (let i = order.length - 2; i >= 0; i–) { const cur = order[i]; const next = order[i + 1]; if (selectedImgs.has(cur) && !selectedImgs.has(next)) { [floatImgs[cur].zIndex, floatImgs[next].zIndex] = [floatImgs[next].zIndex, floatImgs[cur].zIndex]; floatImgs[cur].dom.style.zIndex = floatImgs[cur].zIndex; floatImgs[next].dom.style.zIndex = floatImgs[next].zIndex; } } refreshList(); }; // 透明度滑块 slider.oninput = e => { if (selectedImgs.size === 0) return; saveImgState(); const val = parseFloat(e.target.value) / 100; if (batchAdjustMode) selectedImgs.forEach(i => { floatImgs[i].opacity = val; floatImgs[i].dom.style.opacity = val; }); else if (selectedImgs.size === 1) { const i = […selectedImgs][0]; floatImgs[i].opacity = val; floatImgs[i].dom.style.opacity = val; } }; refreshList(); } // 裁剪面板:按钮必触发、用toDataURL、透明度+水印正常 function openCropPanel(imgEl, opacityVal) { const oldCrop = document.querySelector(‘.crop-panel’); if (oldCrop) oldCrop.remove(); lockRatio = 0; const imgOpacity = opacityVal || 1; const panel = document.createElement(‘div’); panel.className = ‘crop-panel’; panel.style.cssText = ` position:fixed;top:0;left:0;width:100vw;height:100vh; background:rgba(0,0,0,0.85);z-index:99999; display:flex;flex-direction:column;align-items:center;justify-content:center; touch-action:none;user-select:none; `; const cropWrap = document.createElement(‘div’); cropWrap.style.cssText = position:relative;max-width:90%;max-height:75vh;; cropImg = document.createElement(‘img’); cropImg.src = imgEl.src; cropImg.style.cssText = max-width:100%;max-height:75vh;display:block;user-select:none;; cropImg.crossOrigin = ‘anonymous’; cropBox = document.createElement(‘div’); cropBox.style.cssText = ` position:absolute;top:40px;left:40px;width:220px;height:220px; border:2px solid #fff;box-shadow:0 0 0 9999px rgba(0,0,0,0.45); cursor:move;box-sizing:border-box; `; // 四角手柄 const handles = [‘tl’,’tr’,’bl’,’br’]; const hs = 20; handles.forEach(dir => { const h = document.createElement(‘div’); h.dataset.dir = dir; h.style.cssText = ` position:absolute;width:${hs}px;height:${hs}px; background:#fff;border:1px solid #666; z-index:10;touch-action:none;pointer-events:auto !important; ; switch(dir){ case'tl':h.style.top=-${hs/2}px;h.style.left=-${hs/2}px;h.style.cursor='nw-resize';break; case'tr':h.style.top=-${hs/2}px;h.style.right=-${hs/2}px;h.style.cursor='ne-resize';break; case'bl':h.style.bottom=-${hs/2}px;h.style.left=-${hs/2}px;h.style.cursor='sw-resize';break; case'br':h.style.bottom=-${hs/2}px;h.style.right=-${hs/2}px`;h.style.cursor=’se-resize’;break; } cropBox.appendChild(h); }); // 比例按钮组 const ratioBar = document.createElement(‘div’); ratioBar.style.cssText = margin-bottom:8px;display:flex;gap:6px;flex-wrap:wrap;justify-content:center;; const ratioBtns = [ {txt:’自由’,val:0}, {txt:’1:1’,val:1}, {txt:’4:3横’,val:2}, {txt:’3:4竖’,val:4}, {txt:’16:9横’,val:3}, {txt:’9:16竖’,val:5} ]; ratioBtns.forEach(item=>{ const btn = document.createElement(‘button’); btn.textContent = item.txt; btn.dataset.r = item.val; btn.style.cssText = padding:5px 8px;border-radius:4px;border:none;background:#eee;color:#333;font-size:12px;white-space:nowrap;; btn.onclick = ()=>{ lockRatio = item.val; ratioBar.querySelectorAll(‘button’).forEach(b=>{ b.style.background = ‘#eee’; b.style.color=’#333’; }); btn.style.background = ‘#2196f3’; btn.style.color = ‘#fff’; }; ratioBar.appendChild(btn); }); ratioBar.querySelector(‘[data-r=”0”]’).style.background = ‘#2196f3’; ratioBar.querySelector(‘[data-r=”0”]’).style.color = ‘#fff’; // 水印区域 const waterBar = document.createElement(‘div’); waterBar.style.cssText = margin-bottom:12px;display:flex;align-items:center;gap:8px;color:#fff;font-size:14px;justify-content:center;; const waterCheck = document.createElement(‘input’); waterCheck.type = ‘checkbox’; waterCheck.id = ‘waterCheck’; const waterLabel = document.createElement(‘label’); waterLabel.htmlFor = ‘waterCheck’; waterLabel.textContent = ‘添加水印’; const waterInput = document.createElement(‘input’); waterInput.type = ‘text’; waterInput.placeholder = ‘请输入水印文字’; waterInput.style.cssText = padding:4px;width:160px;border-radius:4px;border:none;outline:none;font-size:13px;; waterInput.value = ‘Watermark’; waterBar.appendChild(waterCheck); waterBar.appendChild(waterLabel); waterBar.appendChild(waterInput); // 按钮组 const btnBox = document.createElement(‘div’); btnBox.style.cssText = margin-top:12px;display:flex;gap:16px;pointer-events:auto !important;; const saveBtn = document.createElement(‘button’); saveBtn.textContent = ‘裁剪并下载’; saveBtn.style.cssText = padding:8px 20px;font-size:14px;border-radius:6px;border:none;background:#2196f3;color:#fff;pointer-events:auto !important;; const closeBtn = document.createElement(‘button’); closeBtn.textContent = ‘取消’; closeBtn.style.cssText = padding:8px 20px;font-size:14px;border-radius:6px;border:1px solid #ccc;background:#fff;pointer-events:auto !important;; btnBox.append(saveBtn, closeBtn); panel.append(ratioBar, waterBar, cropWrap, btnBox); cropWrap.append(cropImg, cropBox); document.body.appendChild(panel); const getPos = e => e.touches ? e.touches[0] : e; const minSize = 60; // 拖动选区 function startDrag(e) { if (e.target.dataset.dir) return; e.preventDefault(); const p = getPos(e); cropIsDrag = true; resizeDir = ‘’; startX = p.clientX; startY = p.clientY; startL = parseInt(cropBox.style.left); startT = parseInt(cropBox.style.top); } cropBox.addEventListener(‘mousedown’, startDrag); cropBox.addEventListener(‘touchstart’, startDrag); // 缩放选区 function startResize(e) { e.preventDefault(); e.stopPropagation(); const p = getPos(e); cropIsDrag = true; resizeDir = e.target.dataset.dir; startX = p.clientX; startY = p.clientY; startL = parseInt(cropBox.style.left); startT = parseInt(cropBox.style.top); startW = parseInt(cropBox.style.width); startH = parseInt(cropBox.style.height); } cropBox.querySelectorAll(‘div[data-dir]’).forEach(h => { h.addEventListener(‘mousedown’, startResize); h.addEventListener(‘touchstart’, startResize); }); // 获取裁剪比例 function getRatio(){ switch(lockRatio){ case 1: return 1; case 2: return 4/3; case 3: return 16/9; case 4: return 3/4; case 5: return 9/16; default: return 0; } } // 移动&缩放逻辑 function onMove(e) { if (!cropIsDrag) return; e.preventDefault(); const p = getPos(e); const dx = p.clientX - startX; const dy = p.clientY - startY; const imgW = cropImg.clientWidth; const imgH = cropImg.clientHeight; const ratio = getRatio(); if (!resizeDir) { let L = startL + dx; let T = startT + dy; L = Math.max(0, Math.min(L, imgW - startW)); T = Math.max(0, Math.min(T, imgH - startH)); cropBox.style.left = L + 'px'; cropBox.style.top = T + 'px'; return; } let L = startL, T = startT, W = startW, H = startH; switch (resizeDir) { case 'br': W = startW + dx; H = startH + dy; break; case 'tl': W = startW - dx; H = startH - dy; L = startL + dx; T = startT + dy; break; case 'tr': W = startW + dx; H = startH - dy; T = startT + dy; break; case 'bl': W = startW - dx; H = startH + dy; L = startL + dx; break; } if(ratio > 0){ if(Math.abs(dx) > Math.abs(dy)) H = W / ratio; else W = H * ratio; } W = Math.max(minSize, W); H = Math.max(minSize, H); L = Math.max(0, L); T = Math.max(0, T); W = Math.min(W, imgW - L); H = Math.min(H, imgH - T); cropBox.style.left = L + 'px'; cropBox.style.top = T + 'px'; cropBox.style.width = W + 'px'; cropBox.style.height = H + 'px'; } function onEnd() { cropIsDrag = false; resizeDir = ‘’; } document.addEventListener(‘mousemove’, onMove); document.addEventListener(‘touchmove’, onMove); document.addEventListener(‘mouseup’, onEnd); document.addEventListener(‘touchend’, onEnd); closeBtn.onclick = () => { panel.remove(); document.removeEventListener(‘mousemove’, onMove); document.removeEventListener(‘touchmove’, onMove); document.removeEventListener(‘mouseup’, onEnd); document.removeEventListener(‘touchend’, onEnd); }; // 裁剪下载 let saving = false; saveBtn.onclick = (e) => { e.preventDefault(); if (saving) return; saving = true; const needWater = waterCheck.checked; const waterText = waterInput.value.trim(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const imgRect = cropImg.getBoundingClientRect(); const boxRect = cropBox.getBoundingClientRect(); const scaleX = cropImg.naturalWidth / cropImg.clientWidth; const scaleY = cropImg.naturalHeight / cropImg.clientHeight; const cx = (boxRect.left - imgRect.left) * scaleX; const cy = (boxRect.top - imgRect.top) * scaleY; const cw = boxRect.width * scaleX; const ch = boxRect.height * scaleY; canvas.width = cw; canvas.height = ch; ctx.clearRect(0, 0, cw, ch); ctx.globalAlpha = imgOpacity; ctx.drawImage(cropImg, cx, cy, cw, ch, 0, 0, cw, ch); ctx.globalAlpha = 1; if (needWater && waterText) { ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = `${Math.max(12, cw * 0.03)}px sans-serif`; ctx.textAlign = 'right'; const pad = 15; ctx.fillText(waterText, cw - pad, ch - pad); } canvas.toBlob(function(blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); const now = new Date(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const h = String(now.getHours()).padStart(2, '0'); const mi = String(now.getMinutes()).padStart(2, '0'); const s = String(now.getSeconds()).padStart(2, '0'); const fileName = `crop_${m}${d}_${h}${mi}${s}.png`; a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setTimeout(() => { saving = false; panel.remove(); document.removeEventListener('mousemove', onMove); document.removeEventListener('touchmove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchend', onEnd); }, 200); }, 'image/png'); }; } function createFloatImage(url, file, opacity) { const container = document.createElement(‘div’); const index = floatImgs.length; container.style.cssText = ` position:fixed;z-index:${IMG_BASE + index * 10};background:#fff; overflow:hidden;opacity:${opacity}; left:${120 + index * 30}px;top:${220 + index * 30}px; pointer-events:auto;box-sizing:border-box; `; const openBtn = document.createElement(‘div’); openBtn.dataset.noDrag = ‘true’; openBtn.style.cssText = ` position:absolute;top:0;left:0;width:24px;height:24px; background:transparent;cursor:pointer; z-index:9999;pointer-events:auto !important; `; openBtn.onclick = openBtn.ontouchstart = e => { e.stopPropagation(); openFloatImgPanel(); }; const closeBtn = document.createElement(‘div’); closeBtn.dataset.noDrag = ‘true’; closeBtn.style.cssText = ` position:absolute;top:0;right:0;width:24px;height:24px; background:transparent;cursor:pointer; z-index:9999;pointer-events:auto !important; `; closeBtn.onclick = closeBtn.ontouchstart = e => { e.stopPropagation(); container.remove(); floatImgs = floatImgs.filter(i => i.dom !== container); }; const img = document.createElement(‘img’); img.style.cssText = width:100%;height:100%;display:block;pointer-events:none;; const tempImg = new Image(); tempImg.onload = () => { const r = Math.min(1, 220 / Math.max(tempImg.width, tempImg.height)); container.style.width = tempImg.width * r + ‘px’; container.style.height = tempImg.height * r + ‘px’; }; if (file) { const r = new FileReader(); r.onload = e => { img.src = tempImg.src = e.target.result; floatImgs.forEach(item => { if(item.dom === container) item.imgSrc = e.target.result; }); }; r.readAsDataURL(file); } else if (url) { img.src = tempImg.src = url; } container.append(openBtn, closeBtn, img); document.body.appendChild(container); floatImgs.push({ dom: container, penetrate: false, opacity: opacity, zIndex: IMG_BASE + index * 10, imgSrc: url || “” }); // 新增图片保存快照 saveImgState(); container.onmousedown = (e) => { if (e.target.closest(‘[data-noDrag]’)) return; if (!batchSelectMode) { openFloatImgPanel(); return; } e.preventDefault(); toggleSelect(index); }; touchDrag(container, container, index); pinchZoom(container, index); } /* 拖拽 */ function touchDrag(trigger, target, index) { let dragStartX, dragStartY, isDrag = false; let startPositions = new Map(); let batchClickX = 0, batchClickY = 0; trigger.onmousedown = (e) => { if (e.target.closest(‘[data-noDrag]’)) return; if (batchSelectMode && !selectedImgs.has(index)) return; isDrag = true; saveImgState(); const rect = target.getBoundingClientRect(); dragStartX = e.clientX - rect.left; dragStartY = e.clientY - rect.top; if (batchSelectMode) { batchClickX = e.clientX; batchClickY = e.clientY; startPositions.clear(); selectedImgs.forEach(i => { const d = floatImgs[i].dom; startPositions.set(i, { left: parseInt(d.style.left), top: parseInt(d.style.top) }); }); } e.preventDefault(); }; trigger.addEventListener(‘touchstart’, (e) => { if (e.target.closest(‘[data-noDrag]’)) return; if (batchSelectMode && !selectedImgs.has(index)) return; if (e.touches.length !== 1) return; saveImgState(); isDrag = true; const touch = e.touches[0]; const rect = target.getBoundingClientRect(); dragStartX = touch.clientX - rect.left; dragStartY = touch.clientY - rect.top; if (batchSelectMode) { batchClickX = touch.clientX; batchClickY = touch.clientY; startPositions.clear(); selectedImgs.forEach(i => { const d = floatImgs[i].dom; startPositions.set(i, { left: parseInt(d.style.left), top: parseInt(d.style.top) }); }); } e.preventDefault(); }); const moveHandler = (e) => { if (!isDrag) return; const t = e.touches ? e.touches[0] : e; if (batchSelectMode) { const dx = t.clientX - batchClickX; const dy = t.clientY - batchClickY; selectedImgs.forEach(i => { const pos = startPositions.get(i); const d = floatImgs[i].dom; d.style.left = (pos.left + dx) + ‘px’; d.style.top = (pos.top + dy) + ‘px’; }); return; } target.style.left = (t.clientX - dragStartX) + ‘px’; target.style.top = (t.clientY - dragStartY) + ‘px’; }; document.addEventListener(‘mousemove’, moveHandler); document.addEventListener(‘touchmove’, moveHandler); const end = () => { isDrag = false; startPositions.clear(); }; document.onmouseup = end; document.addEventListener(‘touchend’, end); } /* 双指缩放【按你要求改回原版逻辑:只判断选中,不拦截批量模式】 */ function pinchZoom(el, index) { let initDist = 0, initW = 0, initH = 0, ratio = 1; el.addEventListener(‘touchstart’, (e) => { // 还原:只判断是否选中当前图,不再全局拦截 batchSelectMode if (batchSelectMode && !selectedImgs.has(index)) return; if (e.touches.length !== 2) return; initDist = getDistance(e.touches[0], e.touches[1]); initW = el.offsetWidth; initH = el.offsetHeight; ratio = initH / initW; }); el.addEventListener(‘touchmove’, (e) => { // 还原:只判断是否选中当前图,不再全局拦截 batchSelectMode if (batchSelectMode && !selectedImgs.has(index)) return; if (e.touches.length !== 2) return; const scale = getDistance(e.touches[0], e.touches[1]) / initDist; const w = Math.max(80, initW * scale); const h = w * ratio; if (batchSelectMode) { selectedImgs.forEach(i => { const d = floatImgs[i].dom; d.style.width = w + ‘px’; d.style.height = h + ‘px’; }); } else { el.style.width = w + ‘px’; el.style.height = h + ‘px’; } }); } function getDistance(p1, p2) { const dx = p2.clientX - p1.clientX; const dy = p2.clientY - p1.clientY; return Math.sqrt(dx * dx + dy * dy); } //悬浮图片↑ // 初始化启动 function init() { initObserver(); if (document.body) { createFloatingButton(); setTimeout(() => { if (fontAdjustSwitch) { setTextSize(textSize); } }, 300); } else { document.addEventListener(“DOMContentLoaded”, function(){ createFloatingButton(); setTimeout(() => { if (fontAdjustSwitch && textSize) { setTextSize(textSize + ‘px’); } }, 300); }); } } init(); })();