// ==UserScript== // @name 链接强制同/新标签打开 // @namespace https://tampermonkey.net/ // @version 2.1.0 // @description 开关【开】= 强制同标签;开关【关】= 强制新标签;支持浮窗折叠/拖动/完全隐藏 // @match *://*/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant unsafeWindow // ==/UserScript== (() => { "use strict"; // true=同标签模式(禁止新标签),false=新标签模式(所有链接新开) const MODE_KEY = "tm_force_same_tab"; // true=显示浮窗,false=隐藏浮窗(只用菜单) const UI_KEY = "tm_ui_visible"; // true=折叠状态(最小化) const MIN_KEY = "tm_ui_minimized"; // 位置 const POS_KEY = "tm_ui_pos"; let forceSameTab = GM_getValue(MODE_KEY, true); let uiVisible = GM_getValue(UI_KEY, true); let minimized = GM_getValue(MIN_KEY, false); let uiPos = GM_getValue(POS_KEY, { right: 12, bottom: 12 }); function syncModeToPage() { try { unsafeWindow.__tm_force_tab_mode__ = forceSameTab ? "same" : "blank"; } catch (_) {} } syncModeToPage(); // ===== 注入到页面上下文:改写 window.open ===== (function injectWindowOpenOverride() { const code = ` (function () { const ORIG_KEY = "__tm_orig_open__"; if (!window[ORIG_KEY]) window[ORIG_KEY] = window.open; function norm(t){ return (t || "").toString().toLowerCase(); } window.open = function(url, target, features) { try { const mode = window.__tm_force_tab_mode__ || "same"; // "same" | "blank" const t = norm(target); if (url != null) { const abs = new URL(url, document.baseURI).href; if (mode === "same") { if (t === "" || t === "_blank" || t === "blank") { window.location.href = abs; return window; } } else { if (t === "" || t === "_self" || t === "self") { return window[ORIG_KEY].call( window, abs, "_blank", (features ? String(features) + "," : "") + "noopener,noreferrer" ); } } } } catch (e) {} return window[ORIG_KEY].call(window, url, target, features); }; })(); `; const s = document.createElement("script"); s.textContent = code; document.documentElement.appendChild(s); s.remove(); })(); // ===== 点击层:拦截所有 导航 ===== function findLinkFromEvent(e) { const t = e.target; if (!t) return null; if (t.closest) return t.closest("a[href]"); if (t.tagName === "AREA" && t.href) return t; return null; } function isSkippableLink(a) { const raw = (a.getAttribute && a.getAttribute("href")) || ""; const href = (a.href || raw || "").toString().trim(); if (a.hasAttribute && a.hasAttribute("download")) return true; const low = href.toLowerCase(); if (!href || low === "#" || low.startsWith("#")) return true; if (low.startsWith("javascript:")) return true; return false; } function navSameTab(url) { try { window.location.href = new URL(url, document.baseURI).href; } catch (_) { window.location.href = url; } } function navNewTab(url) { try { window.open(new URL(url, document.baseURI).href, "_blank", "noopener,noreferrer"); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function onClickCapture(e) { const a = findLinkFromEvent(e); if (!a) return; if (isSkippableLink(a)) return; const href = a.href || a.getAttribute("href"); if (!href) return; e.preventDefault(); e.stopImmediatePropagation(); if (forceSameTab) navSameTab(href); else navNewTab(href); } document.addEventListener("click", onClickCapture, true); document.addEventListener("auxclick", onClickCapture, true); // ===== UI:可折叠 + 可隐藏 + 可拖动 ===== GM_addStyle(` #tm-tabbox { position: fixed; z-index: 2147483647; font: 12px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,"PingFang SC","Microsoft YaHei",sans-serif; user-select: none; } #tm-tabbox .panel { background: rgba(0,0,0,.72); color: #fff; border-radius: 12px; box-shadow: 0 6px 18px rgba(0,0,0,.25); overflow: hidden; } #tm-tabbox .bar { display:flex; align-items:center; gap:8px; padding:10px 10px; cursor: move; white-space: nowrap; } #tm-tabbox .title { font-weight: 600; } #tm-tabbox .state { opacity:.9; } #tm-tabbox .btns { margin-left:auto; display:flex; gap:6px; } #tm-tabbox button { all: unset; cursor:pointer; padding: 4px 8px; border-radius: 8px; background: rgba(255,255,255,.14); } #tm-tabbox button:hover { background: rgba(255,255,255,.22); } #tm-tabbox .body { padding: 10px; display:flex; gap:8px; align-items:center; border-top: 1px solid rgba(255,255,255,.15); } #tm-tabbox .pill { cursor:pointer; padding:8px 10px; border-radius: 10px; background: rgba(255,255,255,.14); white-space: nowrap; } #tm-tabbox .hint { opacity:.8; } /* 最小化:只剩一个小圆点 */ #tm-tabbox.min .panel { border-radius: 999px; } #tm-tabbox.min .bar, #tm-tabbox.min .body { display:none; } #tm-tabbox.min .dot { width: 18px; height: 18px; background: rgba(0,0,0,.72); border-radius: 999px; box-shadow: 0 6px 18px rgba(0,0,0,.25); cursor: pointer; } `); let box, dot; function setModeSameTab(v) { forceSameTab = !!v; GM_setValue(MODE_KEY, forceSameTab); syncModeToPage(); renderUI(); } function toggleMode() { setModeSameTab(!forceSameTab); } function setMin(v) { minimized = !!v; GM_setValue(MIN_KEY, minimized); renderUI(); } function toggleMin() { setMin(!minimized); } function setUIVisible(v) { uiVisible = !!v; GM_setValue(UI_KEY, uiVisible); if (box) box.style.display = uiVisible ? "" : "none"; } function renderUI() { if (!box) return; box.classList.toggle("min", minimized); const stateEl = box.querySelector(".state"); const pillEl = box.querySelector(".pill"); const hintEl = box.querySelector(".hint"); const modeText = forceSameTab ? "同标签" : "新标签"; if (stateEl) stateEl.textContent = `模式:${modeText}`; if (pillEl) pillEl.textContent = forceSameTab ? "切到:新标签" : "切到:同标签"; if (hintEl) hintEl.textContent = forceSameTab ? "当前:所有链接同标签打开" : "当前:所有链接新标签打开"; } function applyPos() { if (!box) return; // 用 right/bottom 存储,避免不同分辨率下跑飞 box.style.right = `${uiPos.right}px`; box.style.bottom = `${uiPos.bottom}px`; } function mountUI() { if (box) return; box = document.createElement("div"); box.id = "tm-tabbox"; box.innerHTML = `
Tab 模式
`; document.documentElement.appendChild(box); dot = box.querySelector(".dot"); dot.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); setMin(false); }, true); box.querySelector(".pill").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); toggleMode(); }, true); box.querySelector(".minbtn").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); toggleMin(); }, true); box.querySelector(".hidebtn").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); setUIVisible(false); }, true); // 拖动:仅拖 bar const bar = box.querySelector(".bar"); let dragging = false; let startX = 0, startY = 0; let startRight = 0, startBottom = 0; const onDown = (e) => { // 只支持鼠标左键拖动 if (e.button !== 0) return; dragging = true; startX = e.clientX; startY = e.clientY; startRight = uiPos.right; startBottom = uiPos.bottom; e.preventDefault(); e.stopPropagation(); }; const onMove = (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; // right/bottom: 鼠标往右 -> right 变小;往下 -> bottom 变小 uiPos.right = Math.max(0, Math.round(startRight - dx)); uiPos.bottom = Math.max(0, Math.round(startBottom - dy)); applyPos(); }; const onUp = () => { if (!dragging) return; dragging = false; GM_setValue(POS_KEY, uiPos); }; bar.addEventListener("mousedown", onDown, true); window.addEventListener("mousemove", onMove, true); window.addEventListener("mouseup", onUp, true); applyPos(); renderUI(); box.style.display = uiVisible ? "" : "none"; } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", mountUI, { once: true }); } else { mountUI(); } // ===== 菜单命令(即使隐藏浮窗也能用) ===== GM_registerMenuCommand("切到:同标签模式(禁止新标签)", () => setModeSameTab(true)); GM_registerMenuCommand("切到:新标签模式(所有链接新开)", () => setModeSameTab(false)); GM_registerMenuCommand("浮窗:显示", () => setUIVisible(true)); GM_registerMenuCommand("浮窗:隐藏", () => setUIVisible(false)); GM_registerMenuCommand("浮窗:最小化/展开", () => toggleMin()); })();