Greasy Fork is available in English.
智能识别+可视化指示器。优化交互:支持"点击-再点击"极速流,新增鼠标移出防抖机制,容错率更高。
当前为
// ==UserScript==
// @name 打开网页:新标签页2 (防抖容错版)
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 智能识别+可视化指示器。优化交互:支持"点击-再点击"极速流,新增鼠标移出防抖机制,容错率更高。
// @author HAZE
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// === UI 配置 (Zen Mode) ===
const AUTO_CLOSE_TIMEOUT = 3500; // 全局自动关闭时间(稍微延长以配合容错)
const MOUSE_LEAVE_DELAY = 800; // 关键修改:鼠标移出后的容错时间 (0.8秒)
// 颜色配置
const COLOR_PRIMARY = '#007AFF'; // macOS Blue
const COLOR_SECONDARY = '#555555';
const COLOR_POPUP_BG = 'rgba(255, 255, 255, 0.90)'; // 稍微增加一点不透明度,提升可见性
// 指示器颜色
const IND_COLOR_POPUP = '#af52de'; // macOS Purple
const IND_COLOR_NEWTAB = '#34c759'; // macOS Green
// === 状态管理 ===
const getCurrentMode = () => GM_getValue('openMode', 'popup');
const getBackgroundMode = () => GM_getValue('backgroundMode', false);
const getIndicatorState = () => GM_getValue('showIndicator', true);
const getCurrentDomain = () => window.location.hostname;
const getExcludedSites = () => GM_getValue('excludedSites', []);
const isCurrentSiteExcluded = () => getExcludedSites().includes(getCurrentDomain());
// === CSS 注入 ===
const style = document.createElement('style');
style.textContent = `
/* 指示器样式 */
a[data-haze-link="active"] { position: relative; }
a[data-haze-link="active"]::after {
content: ""; display: inline-block; width: 5px; height: 5px;
margin-left: 3px; border-radius: 50%; vertical-align: middle;
opacity: 0.5; transition: all 0.2s; pointer-events: none;
}
a[data-haze-link="active"]:hover::after { transform: scale(1.5); opacity: 1; }
.haze-ind-popup::after { background-color: ${IND_COLOR_POPUP}; box-shadow: 0 0 6px ${IND_COLOR_POPUP}; }
.haze-ind-newtab::after { background-color: ${IND_COLOR_NEWTAB}; box-shadow: 0 0 6px ${IND_COLOR_NEWTAB}; }
/* 弹窗动画 */
@keyframes haze-pop-in {
0% { opacity: 0; transform: translate(-65%, -45%) scale(0.95); }
100% { opacity: 1; transform: translate(-65%, -50%) scale(1); }
}
`;
document.head.appendChild(style);
// === 智能链接识别 ===
const isFunctionalLink = (link) => {
const href = link.getAttribute('href');
if (!href || href === '' || href === '#' || href === 'javascript:;' || href.includes('javascript:void')) return true;
if (href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('sms:')) return true;
if (link.target === '_self' || link.target === '_top' || link.target === 'iframe') return true;
const role = link.getAttribute('role');
if (role && ['button', 'tab', 'menuitem', 'option', 'switch', 'checkbox', 'radio', 'treeitem'].includes(role)) return true;
try {
if (href.startsWith('#')) return true;
const urlObj = new URL(link.href);
if (urlObj.pathname === window.location.pathname && urlObj.hash !== '') return true;
} catch (e) {}
const functionalityAttrs = [
'onclick', 'download', 'data-toggle', 'data-trigger', 'data-target', 'data-action', 'data-dismiss', 'data-cmd',
'aria-controls', 'aria-expanded', 'aria-haspopup', 'aria-disabled', 'aria-selected',
'ng-click', '@click', 'v-on:click', ':click', 'hx-get', 'hx-post'
];
for (const attr of functionalityAttrs) {
if (link.hasAttribute(attr)) return true;
}
const text = link.textContent.trim();
if (/^\d+$/.test(text)) return true;
const className = (link.className || '').toLowerCase();
if (className.includes('script-link')) return false;
const strToCheck = (
className + ' ' + (link.id || '') + ' ' + (link.title || '') + ' ' +
(link.getAttribute('aria-label') || '') + ' ' + (link.parentElement ? link.parentElement.className : '')
).toLowerCase();
const functionalKeywords = [
'login', 'logout', 'signin', 'register', 'submit', 'cancel', 'edit', 'delete', 'setting', 'close',
'expand', 'collapse', 'load more', 'next', 'prev', 'filter', 'sort', 'search', 'cart', 'buy',
'sku', 'select', 'like', 'fav', 'share', 'reply', 'comment', 'play', 'pause'
];
if (functionalKeywords.some(kw => strToCheck.includes(kw))) return true;
const cnKeywords = ['登录', '注册', '注销', '提交', '取消', '编辑', '删除', '设置', '更多', '展开', '收起', '筛选', '排序', '购物车', '购买', '点赞', '收藏', '分享', '回复', '评论', '播放'];
const lowerText = text.toLowerCase();
if (lowerText.length <= 5 && cnKeywords.some(kw => lowerText.includes(kw))) return true;
if (cnKeywords.some(kw => strToCheck.includes(kw))) return true;
return false;
};
const isSystemFolderLink = (href) => /^file:\/\/\/[a-zA-Z]:\//.test(href);
// === 指示器逻辑 ===
const updateLinkIndicators = () => {
if (!getIndicatorState() || isCurrentSiteExcluded() || getCurrentMode() === 'default') {
document.querySelectorAll('a[data-haze-link]').forEach(el => {
el.removeAttribute('data-haze-link');
el.className = el.className.replace(/haze-ind-\w+/g, '').trim();
});
return;
}
const mode = getCurrentMode();
const indicatorClass = mode === 'popup' ? 'haze-ind-popup' : 'haze-ind-newtab';
document.querySelectorAll('a:not([data-haze-link])').forEach(link => {
if (!isFunctionalLink(link) && !isSystemFolderLink(link.href)) {
link.setAttribute('data-haze-link', 'active');
link.classList.add(indicatorClass);
}
});
};
const observeDOM = () => {
const observer = new MutationObserver((mutations) => {
if (mutations.some(m => m.type === 'childList' && m.addedNodes.length > 0)) {
setTimeout(updateLinkIndicators, 500);
}
});
observer.observe(document.body, { childList: true, subtree: true });
};
// === UI 渲染 (关键修改:防抖机制) ===
const createLinkOptionsPopup = (event, link) => {
if (isCurrentSiteExcluded() || isFunctionalLink(link) || isSystemFolderLink(link.href)) return;
// 移除已存在的弹窗(防止多开)
const existing = document.getElementById('haze-popup');
if (existing) existing.remove();
const popup = document.createElement('div');
popup.id = 'haze-popup';
Object.assign(popup.style, {
position: 'fixed',
top: `${event.clientY}px`,
left: `${event.clientX}px`,
transform: 'translate(-65%, -50%)', // 保持偏移,确保右侧按钮在鼠标下
backgroundColor: COLOR_POPUP_BG,
backdropFilter: 'blur(12px)',
webkitBackdropFilter: 'blur(12px)',
borderRadius: '10px',
boxShadow: '0 8px 32px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.05)',
border: '1px solid rgba(255,255,255,0.4)',
padding: '5px',
zIndex: '2147483647',
display: 'flex',
gap: '6px',
pointerEvents: 'auto',
animation: 'haze-pop-in 0.15s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards',
boxSizing: 'border-box'
});
const createBtn = (text, isPrimary) => {
const btn = document.createElement('div');
btn.textContent = text;
Object.assign(btn.style, {
padding: '6px 12px',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
fontWeight: isPrimary ? '600' : '400',
color: isPrimary ? COLOR_PRIMARY : COLOR_SECONDARY,
backgroundColor: isPrimary ? 'rgba(0, 122, 255, 0.1)' : 'transparent',
transition: 'all 0.1s ease',
whiteSpace: 'nowrap',
userSelect: 'none',
textAlign: 'center',
minWidth: isPrimary ? '80px' : '60px',
flex: isPrimary ? '1.5' : '1'
});
btn.addEventListener('mouseenter', () => {
btn.style.backgroundColor = isPrimary ? 'rgba(0, 122, 255, 0.2)' : 'rgba(0,0,0,0.05)';
btn.style.transform = 'translateY(-1px)';
});
btn.addEventListener('mouseleave', () => {
btn.style.backgroundColor = isPrimary ? 'rgba(0, 122, 255, 0.1)' : 'transparent';
btn.style.transform = 'translateY(0)';
});
return btn;
};
const btnCurrent = createBtn('🏠 当前', false);
const isBg = getBackgroundMode();
const btnNewTab = createBtn(isBg ? '🚀 后台' : '↗ 新标签', true);
btnCurrent.onclick = (e) => {
e.stopPropagation();
window.location.href = link.href;
popup.remove();
};
btnNewTab.onclick = (e) => {
e.stopPropagation();
if (getBackgroundMode()) {
GM_openInTab(link.href, { active: false, insert: true, setParent: true });
} else {
window.open(link.href, '_blank');
}
popup.remove();
};
popup.appendChild(btnCurrent);
popup.appendChild(btnNewTab);
document.body.appendChild(popup);
// === 智能防抖关闭逻辑 (关键) ===
let autoCloseTimer = setTimeout(() => { if (popup.parentNode) popup.remove(); }, AUTO_CLOSE_TIMEOUT);
let leaveTimer = null;
popup.addEventListener('mouseenter', () => {
// 鼠标进入,清除所有关闭倒计时
clearTimeout(autoCloseTimer);
if (leaveTimer) clearTimeout(leaveTimer);
});
popup.addEventListener('mouseleave', () => {
// 鼠标离开,启动容错倒计时 (800ms)
// 如果用户只是手滑并在800ms内移回,这里会被 mouseenter 的 clear 清除,从而不关闭
leaveTimer = setTimeout(() => {
if (popup.parentNode) popup.remove();
}, MOUSE_LEAVE_DELAY);
});
};
// === 事件监听 ===
const handleLinkClick = (event) => {
if (isCurrentSiteExcluded()) return;
const link = event.target.closest('a');
if (!link || !link.href) return;
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
if (isFunctionalLink(link) || isSystemFolderLink(link.href)) return;
const currentMode = getCurrentMode();
if (currentMode === 'popup') {
event.preventDefault();
event.stopPropagation();
createLinkOptionsPopup(event, link);
} else if (currentMode === 'newtab') {
event.preventDefault();
event.stopPropagation();
if (getBackgroundMode()) {
GM_openInTab(link.href, { active: false, insert: true, setParent: true });
} else {
window.open(link.href, '_blank');
}
}
};
// === 菜单注册 ===
let menuIds = [];
const registerMenu = () => {
menuIds.forEach(id => GM_unregisterMenuCommand(id));
menuIds = [];
const mode = getCurrentMode();
const isBg = getBackgroundMode();
const showInd = getIndicatorState();
const modeText = {
'popup': '🟣 模式:选择框 (当前)',
'newtab': '🟢 模式:直接新标签',
'default': '⚪ 模式:浏览器默认'
};
menuIds.push(GM_registerMenuCommand(modeText[mode], () => {
const next = mode === 'popup' ? 'newtab' : (mode === 'newtab' ? 'default' : 'popup');
GM_setValue('openMode', next);
location.reload();
}));
if (mode !== 'default') {
menuIds.push(GM_registerMenuCommand(isBg ? '⚙️ 新标签:后台静默' : '⚙️ 新标签:前台跳转', () => {
GM_setValue('backgroundMode', !isBg);
registerMenu();
}));
}
menuIds.push(GM_registerMenuCommand(showInd ? '👁️ 指示器:开启' : '👁️ 指示器:关闭', () => {
GM_setValue('showIndicator', !showInd);
location.reload();
}));
if (isCurrentSiteExcluded()) {
menuIds.push(GM_registerMenuCommand(`✅ 恢复此网站`, () => {
const s = getExcludedSites();
s.splice(s.indexOf(getCurrentDomain()), 1);
GM_setValue('excludedSites', s);
location.reload();
}));
} else {
menuIds.push(GM_registerMenuCommand(`🚫 排除此网站`, () => {
const s = getExcludedSites();
s.push(getCurrentDomain());
GM_setValue('excludedSites', s);
location.reload();
}));
}
};
const init = () => {
document.addEventListener('click', handleLinkClick, true);
registerMenu();
updateLinkIndicators();
observeDOM();
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();