// ==UserScript== // @name 网页文字提取编辑器 // @namespace universal // @version 1.1 // @description 可拖拽按钮+可拖拽面板+全局缓存坐标+贴边隐藏+边缘滑出+按钮不变形+刷新文字功能 // @match *://*/* // @grant none // @author wangjiahao111 // @license MIT // @run-at document_start // ==/UserScript== (function() { 'use strict'; let originList = []; let highlightTimer = null; const EDGE_HOT_ZONE = 18; let isDockedLeft = false; let isDockedRight = false; // ================================================================================ // 🔥 修复 Bug1:跨域名全局坐标(通过固定名称+兼容所有存储环境) // ================================================================================ function getGlobalStorage() { return window.localStorage; } function saveGlobalPos(key, x, y) { const storage = getGlobalStorage(); x = Math.round(x); y = Math.round(y); storage.setItem('@GLOBAL_UI_POS_' + key, JSON.stringify({ x, y })); } function loadGlobalPos(key) { try { const data = getGlobalStorage().getItem('@GLOBAL_UI_POS_' + key); return data ? JSON.parse(data) : { x: null, y: null }; } catch (e) { return { x: null, y: null }; } } // ================================================================================ // 防止重复创建 // ================================================================================ if (window._textEditorLoaded) return; window._textEditorLoaded = true; // ================================================================================ // 主悬浮按钮 // ================================================================================ const dragWrap = document.createElement('div'); dragWrap.style.cssText = ` position: fixed; z-index: 99999999; user-select: none; transition: transform 0.2s ease; width: 80px; box-sizing: border-box; `; const dragHead = document.createElement('div'); dragHead.innerText = '≡ 拖拽'; dragHead.style.cssText = ` padding: 7px 10px; background: #2d8cf0; color: #fff; text-align: center; border-radius: 6px 6px 0 0; cursor: grab; font-size: 12px; white-space: nowrap; box-sizing: border-box; `; const mainBtn = document.createElement('div'); mainBtn.innerText = '编辑文字'; mainBtn.style.cssText = ` padding: 7px 10px; background: #409eff; color: #fff; text-align: center; cursor: pointer; font-size: 12px; border-radius: 0 0 6px 6px; white-space: nowrap; box-sizing: border-box; `; dragWrap.appendChild(dragHead); dragWrap.appendChild(mainBtn); document.documentElement.appendChild(dragWrap); // ================================================================================ // 编辑面板 // ================================================================================ const panel = document.createElement('div'); panel.style.cssText = ` position: fixed; z-index: 9999998; width: 540px; height: 680px; max-width: 95vw; max-height: 95vh; background: #fff; border-radius: 12px; box-shadow: 0 6px 30px rgba(0,0,0,0.3); padding: 18px; display: none; flex-direction: column; gap: 12px; box-sizing: border-box; `; const panelDragHead = document.createElement('div'); panelDragHead.innerText = '✏️ 文字编辑面板(可拖拽)'; panelDragHead.style.cssText = ` padding: 8px; background: #2d8cf0; color: #fff; text-align: center; border-radius: 6px; cursor: grab; font-size: 13px; margin: -18px -18px 10px -18px; box-sizing: border-box; `; const tip = document.createElement('div'); tip.innerText = '✅ 仅可修改框内文字|选择器已锁定|双击定位'; tip.style.fontSize = '12px'; tip.style.color = '#f56c6c'; tip.style.fontWeight = 'bold'; const editor = document.createElement('textarea'); editor.style.cssText = ` flex:1; padding:14px; font-size:13px; border:1px solid #ddd; border-radius:8px; resize:none; font-family:monospace; line-height:1.7; box-sizing:border-box; `; const bar = document.createElement('div'); bar.style.display = 'flex'; bar.style.gap = '8px'; const refreshBtn = document.createElement('button'); refreshBtn.innerText = '🔄 刷新文字'; refreshBtn.style.cssText = 'flex:1; padding:10px; background:#60c260; color:#fff; border:none; border-radius:6px;cursor:pointer;'; const applyBtn = document.createElement('button'); applyBtn.innerText = '✅ 应用修改'; applyBtn.style.cssText = 'flex:1; padding:10px; background:#00c48c; color:#fff; border:none; border-radius:6px;cursor:pointer;'; const closeBtn = document.createElement('button'); closeBtn.innerText = '关闭'; closeBtn.style.cssText = 'flex:1; padding:10px; background:#f5f5f5; border:1px solid #ddd; border-radius:6px;cursor:pointer;'; bar.append(refreshBtn, applyBtn, closeBtn); panel.append(panelDragHead, tip, editor, bar); document.documentElement.appendChild(panel); // ================================================================================ // 安全位置限制(修复:永不消失) // ================================================================================ function clamp(val, min, max) { return Math.max(min, Math.min(val, max)); } function applyBtnSafePos() { const pos = loadGlobalPos('BTN_POS'); const winW = window.innerWidth; const winH = window.innerHeight; const w = 80; const h = dragWrap.offsetHeight || 60; let x = pos.x ?? winW - 120; let y = pos.y ?? 80; x = clamp(x, 0, winW - w); y = clamp(y, 0, winH - h); dragWrap.style.left = x + 'px'; dragWrap.style.top = y + 'px'; dragWrap.style.right = 'auto'; dragWrap.style.transform = 'translateX(0)'; isDockedLeft = isDockedRight = false; } function applyPanelSafePos() { const pos = loadGlobalPos('PANEL_POS'); const winW = window.innerWidth; const winH = window.innerHeight; const w = Math.min(540, panel.offsetWidth || 540); const h = Math.min(680, panel.offsetHeight || 680); let x = pos.x ?? (winW - w) / 2; let y = pos.y ?? (winH - h) / 2; x = clamp(x, 0, winW - w); y = clamp(y, 0, winH - h); panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.transform = 'none'; } // ================================================================================ // 主按钮拖拽 // ================================================================================ let isBtnDrag = false; dragHead.onmousedown = (e) => { isBtnDrag = true; const sx = e.clientX, sy = e.clientY; const rect = dragWrap.getBoundingClientRect(); const ox = rect.left, oy = rect.top; e.preventDefault(); e.stopPropagation(); function move(ev) { if (!isBtnDrag) return; let nx = ox + (ev.clientX - sx); let ny = oy + (ev.clientY - sy); nx = clamp(nx, 0, window.innerWidth - 80); ny = clamp(ny, 0, window.innerHeight - 60); dragWrap.style.left = nx + 'px'; dragWrap.style.top = ny + 'px'; dragWrap.style.transform = 'translateX(0)'; isDockedLeft = isDockedRight = false; } function up() { isBtnDrag = false; saveGlobalPos('BTN_POS', dragWrap.getBoundingClientRect().left, dragWrap.getBoundingClientRect().top); applyBtnSafePos(); document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }; // ================================================================================ // 面板拖拽 // ================================================================================ let isPanelDrag = false; panelDragHead.onmousedown = (e) => { isPanelDrag = true; const sx = e.clientX, sy = e.clientY; const rect = panel.getBoundingClientRect(); const ox = rect.left, oy = rect.top; e.preventDefault(); e.stopPropagation(); function move(ev) { if (!isPanelDrag) return; let nx = ox + (ev.clientX - sx); let ny = oy + (ev.clientY - sy); nx = clamp(nx, 0, window.innerWidth - panel.offsetWidth); ny = clamp(ny, 0, window.innerHeight - panel.offsetHeight); panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; } function up() { isPanelDrag = false; saveGlobalPos('PANEL_POS', panel.getBoundingClientRect().left, panel.getBoundingClientRect().top); applyPanelSafePos(); document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }; // ================================================================================ // 贴边隐藏/滑出(修复逻辑,稳定不抖动) // ================================================================================ document.addEventListener('mousemove', (e) => { if (isBtnDrag || isPanelDrag) return; const x = e.clientX; const winW = window.innerWidth; const leftZone = x < EDGE_HOT_ZONE; const rightZone = x > winW - EDGE_HOT_ZONE; if (leftZone) { isDockedLeft = true; isDockedRight = false; dragWrap.style.left = '0px'; dragWrap.style.transform = 'translateX(0)'; } else if (rightZone) { isDockedRight = true; isDockedLeft = false; dragWrap.style.left = 'auto'; dragWrap.style.right = '0px'; dragWrap.style.transform = 'translateX(0)'; } else { if (isDockedLeft) dragWrap.style.transform = 'translateX(-100%)'; if (isDockedRight) dragWrap.style.transform = 'translateX(100%)'; } }); // ================================================================================ // 初始化 // ================================================================================ window.addEventListener('load', () => { applyBtnSafePos(); applyPanelSafePos(); }); // ================================================================================ // 文字提取 // ================================================================================ function extractAllText() { originList = []; const list = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); while (walker.nextNode()) { const node = walker.currentNode; const parent = node.parentElement; if (!parent) continue; const style = getComputedStyle(parent); if (style.display === 'none' || style.visibility === 'hidden') continue; if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'BUTTON', 'IFRAME'].includes(parent.tagName)) continue; const text = node.textContent.trim(); if (!text) continue; let selector = ''; try { const path = []; let el = parent; while (el && el.nodeType === 1 && el !== document.documentElement) { const idx = Array.from(el.parentElement.children).indexOf(el) + 1; path.unshift(el.tagName.toLowerCase() + `:nth-child(${idx})`); el = el.parentElement; } selector = path.join(' > '); } catch (e) {} originList.push({ node, oldText: text, selector }); list.push(`┌────────────────────┐\n│ ${text}\n└────────────────────┘\n【锁定选择器】${selector}\n\n`); } return list.join(''); } // ================================================================================ // 高亮 & 锁定 & 刷新 & 应用 // ================================================================================ function clearHighlight() { document.querySelectorAll('[data-editor-highlight]').forEach(el => { el.style.outline = ''; delete el.dataset.editorHighlight; }); if (highlightTimer) clearTimeout(highlightTimer); } editor.addEventListener('keydown', e => { const v = editor.value; const p = editor.selectionStart; const pre = v.substring(0, p); if (pre.lastIndexOf('【锁定选择器】') > pre.lastIndexOf('└────────────────────┘')) e.preventDefault(); }); editor.addEventListener('paste', e => { const v = editor.value; const p = editor.selectionStart; const pre = v.substring(0, p); if (pre.lastIndexOf('【锁定选择器】') > pre.lastIndexOf('└────────────────────┘')) e.preventDefault(); }); function refreshPageText() { editor.value = extractAllText(); } function applyOnlyModified() { clearHighlight(); const content = editor.value; const reg = /┌────────────────────┐\s*│\s*(.*?)\s*└────────────────────┘\s*【锁定选择器】([^\n]+)/gs; let count = 0, match; while ((match = reg.exec(content)) !== null) { const newText = match[1].trim(); const sel = match[2].trim(); const item = originList[count++]; if (!item || item.selector !== sel || item.oldText === newText) continue; try { item.node.textContent = newText; item.oldText = newText; } catch (e) {} } alert(`✅ 修改成功`); } editor.ondblclick = () => { clearHighlight(); const p = editor.selectionStart; const aft = editor.value.slice(p); const m = aft.match(/【锁定选择器】([^\n]+)/); if (m && m[1]) { const el = document.querySelector(m[1].trim()); if (el) { el.dataset.editorHighlight = 1; el.style.outline = '3px solid #ff4444'; el.scrollIntoView({ behavior: 'smooth', block: 'center' }); highlightTimer = setTimeout(clearHighlight, 2000); } } }; mainBtn.onclick = () => { panel.style.display = 'flex'; refreshPageText(); }; refreshBtn.onclick = refreshPageText; applyBtn.onclick = applyOnlyModified; closeBtn.onclick = () => { panel.style.display = 'none'; }; })();