JGY
「创建博客」 「编辑」 「本文源码」

替换脚本


// ==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?'✅启用':'❌禁用'}

`; 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 = `

; 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(); })();


如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

类似帖子

上一篇 嗅探脚本

下一篇 替换脚本

发布评论

smoothScroll.init({ speed: 500, // Integer. How fast to complete the scroll in milliseconds easing: 'easeInOutCubic', // Easing pattern to use offset: 20, // Integer. How far to offset the scrolling anchor location in pixels });