MRBANK 发表于 2026-2-7 22:05

【油猴脚本】右键链接增强 - 快速复制链接文字、地址及多种格式

本帖最后由 MRBANK 于 2026-2-8 15:36 编辑

右键链接增强 - 快速复制链接文字、地址及多种格式

它能做什么?
[*]复制链接文字 - 再也不用手动选中了!
[*]复制链接地址 - 直接获取完整URL
[*]文字+链接 - 方便保存或分享
[*]Markdown格式 - [文字](链接) 一键生成
[*]HTML格式 - <a href="链接">文字</a> 直接复制
[*]BBCode格式 - 文字 论坛发帖神器
[*]复制选中文字 - 选中后右键链接就能复制文字
[*]新标签页打开 - 还是熟悉的操作


与浏览器右键菜单不干扰,空白处右键还是会出现浏览器右键菜单。

效果:


// ==UserScript==
// @name         右键链接增强
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description右键链接快速复制文字、地址及多种格式,智能识别选中文本
// @author       MRBANK
// @match      *://*/*
// @grant      GM_setClipboard
// @grant      GM_openInTab
// @grant      GM_notification
// @icon         https://img.icons8.com/color/96/000000/copy-link.png
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 设计配置
    const DESIGN = {
      menuWidth: 240,
      borderRadius: 8,
      animationSpeed: 100,
      colors: {
            primary: '#3b82f6',
            success: '#10b981',
            error: '#ef4444',
            background: '#ffffff',
            hover: '#f3f4f6',
            border: '#e5e7eb',
            text: '#111827',
            subtitle: '#6b7280'
      },
      shadows: {
            menu: '0 6px 16px rgba(0, 0, 0, 0.12)',
            notification: '0 4px 12px rgba(0, 0, 0, 0.1)'
      },
      fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
    };

    // 状态变量
    let currentLink = null;
    let selectedText = '';
    let menuElement = null;
    let isMenuVisible = false;

    // 初始化
    function init() {
      injectStyles();
      createMenu();
      attachEventListeners();
      console.log('链接右键智能增强已加载');
    }

    // 注入CSS样式
    function injectStyles() {
      const style = document.createElement('style');
      style.textContent = `
            #link-menu {
                position: fixed;
                z-index: 999999;
                background: ${DESIGN.colors.background};
                border-radius: ${DESIGN.borderRadius}px;
                box-shadow: ${DESIGN.shadows.menu};
                font-family: ${DESIGN.fontFamily};
                width: ${DESIGN.menuWidth}px;
                opacity: 0;
                transform: scale(0.95);
                transition: all ${DESIGN.animationSpeed}ms ease-out;
                border: 1px solid ${DESIGN.colors.border};
                overflow: hidden;
                display: none;
                padding: 6px 0;
            }

            #link-menu.visible {
                opacity: 1;
                transform: scale(1);
            }

            .menu-group {
                padding: 4px 0;
                border-bottom: 1px solid ${DESIGN.colors.border};
            }

            .menu-group:last-child {
                border-bottom: none;
            }

            .group-title {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                padding: 6px 12px 4px;
                font-weight: 500;
                letter-spacing: 0.5px;
                text-transform: uppercase;
            }

            .menu-item {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 8px 12px;
                cursor: pointer;
                transition: background-color 0.15s ease;
                font-size: 13px;
                color: ${DESIGN.colors.text};
            }

            .menu-item:hover {
                background-color: ${DESIGN.colors.hover};
            }

            .item-icon {
                width: 16px;
                height: 16px;
                display: flex;
                align-items: center;
                justify-content: center;
                flex-shrink: 0;
                font-size: 14px;
                color: ${DESIGN.colors.primary};
            }

            .item-label {
                flex: 1;
                font-size: 13px;
            }

            .item-hint {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                margin-left: 8px;
                background: ${DESIGN.colors.hover};
                padding: 2px 6px;
                border-radius: 3px;
            }

            /* 通知样式 */
            .menu-notification {
                position: fixed;
                top: 16px;
                right: 16px;
                background: ${DESIGN.colors.background};
                border-radius: 6px;
                box-shadow: ${DESIGN.shadows.notification};
                padding: 10px 12px;
                z-index: 1000000;
                transform: translateX(120%);
                transition: transform 0.25s ease;
                border-left: 3px solid ${DESIGN.colors.primary};
                max-width: 260px;
                display: flex;
                align-items: flex-start;
                gap: 8px;
            }

            .menu-notification.show {
                transform: translateX(0);
            }

            .notification-success {
                border-left-color: ${DESIGN.colors.success};
            }

            .notification-error {
                border-left-color: ${DESIGN.colors.error};
            }

            .notification-icon {
                width: 14px;
                height: 14px;
                flex-shrink: 0;
                font-weight: bold;
                font-size: 12px;
                margin-top: 1px;
            }

            .notification-content {
                flex: 1;
            }

            .notification-title {
                font-weight: 600;
                font-size: 12px;
                color: ${DESIGN.colors.text};
                margin-bottom: 2px;
            }

            .notification-message {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                line-height: 1.3;
            }

            /* 动画效果 */
            @keyframes itemFadeIn {
                from {
                  opacity: 0;
                  transform: translateX(-5px);
                }
                to {
                  opacity: 1;
                  transform: translateX(0);
                }
            }

            .animate-in {
                animation: itemFadeIn 0.15s ease forwards;
                opacity: 0;
            }
      `;
      document.head.appendChild(style);
    }

    // 创建菜单
    function createMenu() {
      // 移除已存在的菜单
      const existingMenu = document.getElementById('link-menu');
      if (existingMenu) existingMenu.remove();

      // 创建新菜单
      menuElement = document.createElement('div');
      menuElement.id = 'link-menu';
      menuElement.innerHTML = `
            <div class="menu-group">
                <div class="group-title">链接操作</div>
                <div class="menu-item" data-action="copy-link-text">
                  <div class="item-icon">&#128221;</div>
                  <div class="item-label">复制链接文字</div>
                </div>
                <div class="menu-item" data-action="copy-link-url">
                  <div class="item-icon">&#128279;</div>
                  <div class="item-label">复制链接地址</div>
                </div>
                <div class="menu-item" data-action="copy-text-url">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">文字 + 链接</div>
                </div>
            </div>

            <div class="menu-group">
                <div class="group-title">格式化复制</div>
                <div class="menu-item" data-action="copy-markdown">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">Markdown格式</div>
                  <div class="item-hint">[文字](链接)</div>
                </div>
                <div class="menu-item" data-action="copy-html">
                  <div class="item-icon">&#127760;</div>
                  <div class="item-label">HTML格式</div>
                  <div class="item-hint">&lt;a&gt;标签</div>
                </div>
                <div class="menu-item" data-action="copy-bbcode">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">BBCode格式</div>
                  <div class="item-hint">标签</div>
                </div>
            </div>

            <!-- 文本操作分组 - 默认隐藏 -->
            <div class="menu-group" id="text-group" style="display: none;">
                <div class="group-title">文本操作</div>
                <div class="menu-item" data-action="copy-selected-text">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">复制选中文字</div>
                </div>
            </div>

            <div class="menu-group">
                <div class="group-title">网页操作</div>
                <div class="menu-item" data-action="open-new-tab">
                  <div class="item-icon">➕</div>
                  <div class="item-label">新标签页打开</div>
                </div>
                <div class="menu-item" data-action="copy-page-url">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">复制页面地址</div>
                </div>
                <div class="menu-item" data-action="view-source">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">查看源代码</div>
                  <div class="item-hint">Ctrl+U</div>
                </div>
            </div>
      `;

      document.body.appendChild(menuElement);

      // 为菜单项添加点击事件
      menuElement.querySelectorAll('.menu-item').forEach(item => {
            item.addEventListener('click', handleMenuClick);
      });
    }

    // 添加事件监听器
    function attachEventListeners() {
      // 监听右键点击
      document.addEventListener('contextmenu', function(e) {
            const target = e.target;
            const link = target.closest('a');

            if (link) {
                // 阻止默认右键菜单
                e.preventDefault();
                e.stopPropagation();

                // 获取选中文本
                selectedText = window.getSelection().toString().trim();

                // 更新状态
                currentLink = link;

                // 更新菜单显示
                updateMenuItems(selectedText);

                // 显示菜单
                showMenu(e.clientX, e.clientY);

                return false;
            }
      }, true);

      // 点击其他地方隐藏菜单
      document.addEventListener('click', function(e) {
            if (menuElement && !menuElement.contains(e.target) && isMenuVisible) {
                hideMenu();
            }
      }, true);

      // ESC键隐藏菜单
      document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && isMenuVisible) {
                hideMenu();
                e.preventDefault();
            }

            // 快捷键:Ctrl+Shift+C 复制链接文字
            if (e.ctrlKey && e.shiftKey && e.key === 'C' && currentLink) {
                e.preventDefault();
                copyLinkText();
            }

            // 快捷键:Ctrl+Shift+L 复制链接地址
            if (e.ctrlKey && e.shiftKey && e.key === 'L' && currentLink) {
                e.preventDefault();
                copyLinkUrl();
            }
      });

      // 窗口大小变化时重新定位菜单
      window.addEventListener('resize', function() {
            if (isMenuVisible) {
                repositionMenu();
            }
      });
    }

    // 更新菜单项
    function updateMenuItems(text) {
      if (!menuElement) return;

      // 获取文本操作分组
      const textGroup = menuElement.querySelector('#text-group');
      if (textGroup) {
            if (text && text.length > 0) {
                // 有选中文字,显示文本操作分组
                textGroup.style.display = 'block';
            } else {
                // 没有选中文字,隐藏文本操作分组
                textGroup.style.display = 'none';
            }
      }
    }

    // 显示菜单
    function showMenu(x, y) {
      if (!menuElement) return;

      // 显示菜单
      menuElement.style.display = 'block';

      // 设置初始位置
      menuElement.style.left = x + 'px';
      menuElement.style.top = y + 'px';

      // 确保菜单位置正确
      repositionMenu();

      // 添加显示动画
      setTimeout(() => {
            menuElement.classList.add('visible');
            isMenuVisible = true;

            // 添加项目动画
            setTimeout(() => {
                const items = menuElement.querySelectorAll('.menu-item:not()');
                items.forEach((item, index) => {
                  item.style.animationDelay = `${index * 0.012}s`;
                  item.classList.add('animate-in');
                });
            }, 10);
      }, 10);
    }

    // 重新定位菜单
    function repositionMenu() {
      if (!menuElement) return;

      const rect = menuElement.getBoundingClientRect();
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      let left = parseInt(menuElement.style.left) || 0;
      let top = parseInt(menuElement.style.top) || 0;

      // 调整水平位置
      if (left + rect.width > windowWidth) {
            left = Math.max(10, windowWidth - rect.width - 10);
      }

      // 调整垂直位置
      if (top + rect.height > windowHeight) {
            top = Math.max(10, windowHeight - rect.height - 10);
      }

      // 应用新位置
      menuElement.style.left = left + 'px';
      menuElement.style.top = top + 'px';
    }

    // 隐藏菜单
    function hideMenu() {
      if (menuElement) {
            menuElement.classList.remove('visible');
            isMenuVisible = false;

            // 移除项目动画类
            menuElement.querySelectorAll('.menu-item').forEach(item => {
                item.classList.remove('animate-in');
                item.style.animationDelay = '';
            });

            // 延迟隐藏
            setTimeout(() => {
                if (!isMenuVisible) {
                  menuElement.style.display = 'none';
                  currentLink = null;
                  selectedText = '';
                }
            }, 100);
      }
    }

    // 处理菜单点击
    function handleMenuClick(e) {
      const action = this.getAttribute('data-action');

      switch(action) {
            // 链接操作
            case 'copy-link-text':
                copyLinkText();
                break;
            case 'copy-link-url':
                copyLinkUrl();
                break;
            case 'copy-text-url':
                copyTextAndUrl();
                break;

            // 格式化复制
            case 'copy-markdown':
                copyAsMarkdown();
                break;
            case 'copy-html':
                copyAsHtml();
                break;
            case 'copy-bbcode':
                copyAsBBCode();
                break;

            // 文本操作
            case 'copy-selected-text':
                copySelectedText();
                break;

            // 网页操作
            case 'open-new-tab':
                openNewTab();
                break;
            case 'copy-page-url':
                copyPageUrl();
                break;
            case 'view-source':
                viewSource();
                break;
      }

      hideMenu();
    }

    // ========== 功能函数 ==========

    // 获取链接文字
    function getLinkText(link) {
      let text = link.innerText || link.textContent || '';
      text = text.replace(/\s+/g, ' ').trim();

      if (!text) {
            text = link.getAttribute('title') ||
                   link.getAttribute('aria-label') ||
                   link.querySelector('img')?.getAttribute('alt') || '';
      }

      return text;
    }

    // 1. 复制链接文字
    function copyLinkText() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const text = getLinkText(currentLink);
      if (text) {
            GM_setClipboard(text);
            showNotification('复制成功', '已复制链接文字');
      } else {
            showNotification('错误', '链接没有文字内容', 'error');
      }
    }

    // 2. 复制链接地址
    function copyLinkUrl() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const url = currentLink.href;
      if (url) {
            GM_setClipboard(url);
            showNotification('复制成功', '已复制链接地址');
      } else {
            showNotification('错误', '链接没有地址', 'error');
      }
    }

    // 3. 复制文字和链接
    function copyTextAndUrl() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const text = getLinkText(currentLink);
      const url = currentLink.href;

      if (text && url) {
            const content = `${text} ${url}`;
            GM_setClipboard(content);
            showNotification('复制成功', '已复制文字和链接');
      } else {
            showNotification('错误', '无法复制文字和链接', 'error');
      }
    }

    // 4. 复制为Markdown格式
    function copyAsMarkdown() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const text = getLinkText(currentLink);
      const url = currentLink.href;

      if (text && url) {
            const content = `[${text}](${url})`;
            GM_setClipboard(content);
            showNotification('复制成功', '已复制为Markdown格式');
      } else {
            showNotification('错误', '无法生成Markdown格式', 'error');
      }
    }

    // 5. 复制为HTML格式
    function copyAsHtml() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const text = getLinkText(currentLink);
      const url = currentLink.href;

      if (text && url) {
            const content = `<a href="${url}">${text}</a>`;
            GM_setClipboard(content);
            showNotification('复制成功', '已复制为HTML格式');
      } else {
            showNotification('错误', '无法生成HTML格式', 'error');
      }
    }

    // 6. 复制为BBCode格式
    function copyAsBBCode() {
      if (!currentLink) {
            showNotification('错误', '没有找到链接', 'error');
            return;
      }

      const text = getLinkText(currentLink);
      const url = currentLink.href;

      if (text && url) {
            const content = `${text}`;
            GM_setClipboard(content);
            showNotification('复制成功', '已复制为BBCode格式');
      } else {
            showNotification('错误', '无法生成BBCode格式', 'error');
      }
    }

    // 7. 复制选中文字
    function copySelectedText() {
      if (selectedText) {
            GM_setClipboard(selectedText);
            showNotification('复制成功', '已复制选中文字');
      } else {
            showNotification('错误', '没有选中文字', 'error');
      }
    }

    // 8. 新标签页打开链接
    function openNewTab() {
      if (currentLink) {
            GM_openInTab(currentLink.href, { active: true });
            showNotification('新标签页', '正在打开链接...');
      } else {
            showNotification('错误', '没有找到链接', 'error');
      }
    }

    // 9. 复制页面地址
    function copyPageUrl() {
      const url = window.location.href;
      GM_setClipboard(url);
      showNotification('复制成功', '已复制页面地址');
    }

    // 10. 查看源代码
    function viewSource() {
      const sourceUrl = `view-source:${window.location.href}`;
      GM_openInTab(sourceUrl, { active: true });
      showNotification('源代码', '正在打开源代码页面...');
    }

    // 显示通知
    function showNotification(title, message, type = 'success') {
      // 移除旧通知
      const oldNotification = document.querySelector('.menu-notification');
      if (oldNotification) {
            oldNotification.remove();
      }

      // 创建新通知
      const notification = document.createElement('div');
      notification.className = `menu-notification notification-${type}`;

      const icon = type === 'success' ? '✓' : '✗';

      notification.innerHTML = `
            <div class="notification-icon">${icon}</div>
            <div class="notification-content">
                <div class="notification-title">${title}</div>
                <div class="notification-message">${message}</div>
            </div>
      `;

      document.body.appendChild(notification);

      // 显示通知
      setTimeout(() => notification.classList.add('show'), 10);

      // 自动隐藏
      setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => {
                if (notification.parentNode) {
                  notification.parentNode.removeChild(notification);
                }
            }, 200);
      }, 1500);

      // 点击隐藏
      notification.addEventListener('click', function() {
            this.classList.remove('show');
            setTimeout(() => {
                if (this.parentNode) {
                  this.parentNode.removeChild(this);
                }
            }, 200);
      });
    }

    // 初始化脚本
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      init();
    }
})();



knian 发表于 2026-3-13 15:49

修复了一下存在的问题

// ==UserScript==
// @name         右键链接增强
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description右键链接快速复制文字、地址及多种格式,智能识别选中文本
// @AuThor       MRBANK
// @match      *://*/*
// @grant      GM_setClipboard
// @grant      GM_openInTab
// @grant      GM_notification
// @Icon         https://img.icons8.com/color/96/000000/copy-link.png
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // 设计配置
    const DESIGN = {
      menuWidth: 240,
      borderRadius: 8,
      animationSpeed: 100,
      colors: {
            primary: '#3b82f6',
            success: '#10b981',
            error: '#ef4444',
            background: '#ffffff',
            backgroundDark: '#1f2937',
            hover: '#f3f4f6',
            hoverDark: '#374151',
            border: '#e5e7eb',
            borderDark: '#374151',
            text: '#111827',
            textDark: '#f9fafb',
            subtitle: '#6b7280',
            subtitleDark: '#9ca3af',
            hintBg: 'rgba(0,0,0,0.07)',
            hintBgDark: 'rgba(255,255,255,0.1)',
      },
      shadows: {
            menu: '0 6px 16px rgba(0, 0, 0, 0.12)',
            notification: '0 4px 12px rgba(0, 0, 0, 0.1)'
      },
      fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
    };

    // 状态变量
    let currentLink = null;
    let selectedText = '';
    let menuElement = null;
    let isMenuVisible = false;

    // 检测暗色模式
    function isDarkMode() {
      return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    // 初始化
    function init() {
      injectStyles();
      createMenu();
      attachEventListeners();
      console.log('链接右键智能增强已加载 v1.1');
    }

    // 注入CSS样式
    function injectStyles() {
      const style = document.createElement('style');
      style.textContent = `
            #link-menu {
                position: fixed;
                /* fix 1: 使用最大 z-index 避免被任何元素遮挡 */
                z-index: 2147483646;
                background: ${DESIGN.colors.background};
                border-radius: ${DESIGN.borderRadius}px;
                box-shadow: ${DESIGN.shadows.menu};
                font-family: ${DESIGN.fontFamily};
                width: ${DESIGN.menuWidth}px;
                opacity: 0;
                transform: scale(0.95);
                transition: opacity ${DESIGN.animationSpeed}ms ease-out, transform ${DESIGN.animationSpeed}ms ease-out;
                border: 1px solid ${DESIGN.colors.border};
                overflow: hidden;
                display: none;
                padding: 6px 0;
            }

            /* fix 2: 暗色模式适配 */
            @media (prefers-color-scheme: dark) {
                #link-menu {
                  background: ${DESIGN.colors.backgroundDark};
                  border-color: ${DESIGN.colors.borderDark};
                  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
                }
            }

            #link-menu.visible {
                opacity: 1;
                transform: scale(1);
            }

            .menu-group {
                padding: 4px 0;
                border-bottom: 1px solid ${DESIGN.colors.border};
            }

            @media (prefers-color-scheme: dark) {
                .menu-group {
                  border-bottom-color: ${DESIGN.colors.borderDark};
                }
            }

            .menu-group:last-child {
                border-bottom: none;
            }

            .group-title {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                padding: 6px 12px 4px;
                font-weight: 500;
                letter-spacing: 0.5px;
                text-transform: uppercase;
            }

            @media (prefers-color-scheme: dark) {
                .group-title {
                  color: ${DESIGN.colors.subtitleDark};
                }
            }

            .menu-item {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 8px 12px;
                cursor: pointer;
                transition: background-color 0.15s ease;
                font-size: 13px;
                color: ${DESIGN.colors.text};
                text-decoration: none !important;
                /* fix 3: 防止长内容撑破菜单宽度 */
                white-space: nowrap;
                overflow: hidden;
            }

            @media (prefers-color-scheme: dark) {
                .menu-item {
                  color: ${DESIGN.colors.textDark};
                }
            }

            .menu-item:hover {
                background-color: ${DESIGN.colors.hover};
            }

            @media (prefers-color-scheme: dark) {
                .menu-item:hover {
                  background-color: ${DESIGN.colors.hoverDark};
                }
            }

            .item-icon {
                width: 16px;
                height: 16px;
                display: flex;
                align-items: center;
                justify-content: center;
                flex-shrink: 0;
                font-size: 14px;
                color: ${DESIGN.colors.primary};
            }

            .item-label {
                flex: 1;
                font-size: 13px;
                overflow: hidden;
                text-overflow: ellipsis;
            }

            .item-hint {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                margin-left: 8px;
                /* fix 4: 使用半透明背景适配亮/暗两种主题 */
                background: ${DESIGN.colors.hintBg};
                padding: 2px 6px;
                border-radius: 3px;
                flex-shrink: 0;
            }

            @media (prefers-color-scheme: dark) {
                .item-hint {
                  color: ${DESIGN.colors.subtitleDark};
                  background: ${DESIGN.colors.hintBgDark};
                }
            }

            /* 通知样式 */
            .menu-notification {
                position: fixed;
                /* fix 5: 通知使用最高 z-index */
                z-index: 2147483647;
                /* fix 6: 改为右下角,减少与顶部导航栏冲突 */
                bottom: 20px;
                right: 16px;
                background: ${DESIGN.colors.background};
                border-radius: 6px;
                box-shadow: ${DESIGN.shadows.notification};
                padding: 10px 12px;
                transform: translateX(120%);
                transition: transform 0.25s ease;
                border-left: 3px solid ${DESIGN.colors.primary};
                max-width: 260px;
                display: flex;
                align-items: flex-start;
                gap: 8px;
            }

            @media (prefers-color-scheme: dark) {
                .menu-notification {
                  background: ${DESIGN.colors.backgroundDark};
                  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
                }
            }

            .menu-notification.show {
                transform: translateX(0);
            }

            .notification-success {
                border-left-color: ${DESIGN.colors.success};
            }

            .notification-error {
                border-left-color: ${DESIGN.colors.error};
            }

            .notification-icon {
                width: 14px;
                height: 14px;
                flex-shrink: 0;
                font-weight: bold;
                font-size: 12px;
                margin-top: 1px;
            }

            .notification-content {
                flex: 1;
            }

            .notification-title {
                font-weight: 600;
                font-size: 12px;
                color: ${DESIGN.colors.text};
                margin-bottom: 2px;
            }

            @media (prefers-color-scheme: dark) {
                .notification-title {
                  color: ${DESIGN.colors.textDark};
                }
            }

            .notification-message {
                font-size: 11px;
                color: ${DESIGN.colors.subtitle};
                line-height: 1.3;
            }

            @media (prefers-color-scheme: dark) {
                .notification-message {
                  color: ${DESIGN.colors.subtitleDark};
                }
            }

            /* 动画效果 */
            @keyframes lm-itemFadeIn {
                from {
                  opacity: 0;
                  transform: translateX(-5px);
                }
                to {
                  opacity: 1;
                  transform: translateX(0);
                }
            }

            .lm-animate-in {
                animation: lm-itemFadeIn 0.15s ease forwards;
                opacity: 0;
            }
      `;
      document.head.appendChild(style);
    }

    // 创建菜单
    function createMenu() {
      const existingMenu = document.getElementById('link-menu');
      if (existingMenu) existingMenu.remove();

      menuElement = document.createElement('div');
      menuElement.id = 'link-menu';

      // fix 7: <a> 标签使用文本节点,避免 innerHTML 解析问题
      menuElement.innerHTML = `
            <div class="menu-group">
                <div class="group-title">链接操作</div>
                <div class="menu-item" data-action="copy-link-text">
                  <div class="item-icon">&#128221;</div>
                  <div class="item-label">复制链接文字</div>
                </div>
                <div class="menu-item" data-action="copy-link-url">
                  <div class="item-icon">&#128279;</div>
                  <div class="item-label">复制链接地址</div>
                </div>
                <div class="menu-item" data-action="copy-text-url">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">文字 + 链接</div>
                </div>
            </div>

            <div class="menu-group">
                <div class="group-title">格式化复制</div>
                <div class="menu-item" data-action="copy-markdown">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">Markdown 格式</div>
                  <div class="item-hint">[文字](链接)</div>
                </div>
                <div class="menu-item" data-action="copy-html">
                  <div class="item-icon">&#127760;</div>
                  <div class="item-label">HTML 格式</div>
                  <div class="item-hint html-tag-hint"></div>
                </div>
                <div class="menu-item" data-action="copy-bbcode">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">BBCode 格式</div>
                  <div class="item-hint">标签</div>
                </div>
            </div>

            <!-- 文本操作分组 - 默认隐藏 -->
            <div class="menu-group" id="text-group" style="display: none;">
                <div class="group-title">文本操作</div>
                <div class="menu-item" data-action="copy-selected-text">
                  <div class="item-icon">&#128203;</div>
                  <div class="item-label">复制选中文字</div>
                </div>
            </div>

            <div class="menu-group">
                <div class="group-title">网页操作</div>
                <div class="menu-item" data-action="open-new-tab">
                  <div class="item-icon">➕</div>
                  <div class="item-label">新标签页打开</div>
                </div>
                <div class="menu-item" data-action="copy-page-url">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">复制页面地址</div>
                </div>
                <div class="menu-item" data-action="view-source">
                  <div class="item-icon">&#128196;</div>
                  <div class="item-label">查看源代码</div>
                  <div class="item-hint">Ctrl+U</div>
                </div>
            </div>
      `;

      // fix 7: 用 textContent 安全写入含 HTML 标签的提示文字
      const htmlHint = menuElement.querySelector('.html-tag-hint');
      if (htmlHint) htmlHint.textContent = '<a>标签';

      document.body.appendChild(menuElement);

      menuElement.querySelectorAll('.menu-item').forEach(item => {
            item.addEventListener('click', handleMenuClick);
      });
    }

    // 添加事件监听器
    function attachEventListeners() {
      document.addEventListener('contextmenu', function (e) {
            const target = e.target;
            const link = target.closest('a');

            if (link) {
                e.preventDefault();
                e.stopPropagation();

                selectedText = window.getSelection().toString().trim();
                currentLink = link;

                updateMenuItems(selectedText);
                showMenu(e.clientX, e.clientY);

                return false;
            }
      }, true);

      document.addEventListener('click', function (e) {
            if (menuElement && !menuElement.contains(e.target) && isMenuVisible) {
                hideMenu();
            }
      }, true);

      document.addEventListener('keydown', function (e) {
            if (e.key === 'Escape' && isMenuVisible) {
                hideMenu();
                e.preventDefault();
            }

            if (e.ctrlKey && e.shiftKey && e.key === 'C' && currentLink) {
                e.preventDefault();
                copyLinkText();
            }

            if (e.ctrlKey && e.shiftKey && e.key === 'L' && currentLink) {
                e.preventDefault();
                copyLinkUrl();
            }
      });

      window.addEventListener('resize', function () {
            if (isMenuVisible) {
                repositionMenu();
            }
      });
    }

    // 更新菜单项
    function updateMenuItems(text) {
      if (!menuElement) return;

      const textGroup = menuElement.querySelector('#text-group');
      if (textGroup) {
            textGroup.style.display = (text && text.length > 0) ? 'block' : 'none';
      }
    }

    // 显示菜单
    function showMenu(x, y) {
      if (!menuElement) return;

      // 先清理上一次的动画 class,再重置为初始状态
      menuElement.querySelectorAll('.menu-item').forEach(item => {
            item.classList.remove('lm-animate-in');
            item.style.animationDelay = '';
      });

      menuElement.style.display = 'block';
      menuElement.style.left = x + 'px';
      menuElement.style.top = y + 'px';

      // fix 8: 用双 rAF 替代 setTimeout,确保 display:block 已完成布局再触发 transition
      requestAnimationFrame(() => {
            repositionMenu();
            requestAnimationFrame(() => {
                menuElement.classList.add('visible');
                isMenuVisible = true;

                const items = menuElement.querySelectorAll('.menu-item');
                items.forEach((item, index) => {
                  item.style.animationDelay = `${index * 0.012}s`;
                  item.classList.add('lm-animate-in');
                });
            });
      });
    }

    // 重新定位菜单
    function repositionMenu() {
      if (!menuElement) return;

      const rect = menuElement.getBoundingClientRect();
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      let left = parseInt(menuElement.style.left) || 0;
      let top = parseInt(menuElement.style.top) || 0;

      if (left + rect.width > windowWidth) {
            left = Math.max(10, windowWidth - rect.width - 10);
      }

      if (top + rect.height > windowHeight) {
            top = Math.max(10, windowHeight - rect.height - 10);
      }

      menuElement.style.left = left + 'px';
      menuElement.style.top = top + 'px';
    }

    // 隐藏菜单
    function hideMenu() {
      if (!menuElement) return;

      menuElement.classList.remove('visible');
      isMenuVisible = false;

      // fix 9: 立即同步清理动画状态,避免下次打开时 delay 残留
      menuElement.querySelectorAll('.menu-item').forEach(item => {
            item.classList.remove('lm-animate-in');
            item.style.animationDelay = '';
      });

      setTimeout(() => {
            if (!isMenuVisible) {
                menuElement.style.display = 'none';
                currentLink = null;
                selectedText = '';
            }
      }, DESIGN.animationSpeed + 20);
    }

    // 处理菜单点击
    function handleMenuClick(e) {
      const action = this.getAttribute('data-action');

      switch (action) {
            case 'copy-link-text':    copyLinkText();      break;
            case 'copy-link-url':   copyLinkUrl();       break;
            case 'copy-text-url':   copyTextAndUrl();    break;
            case 'copy-markdown':   copyAsMarkdown();    break;
            case 'copy-html':         copyAsHtml();      break;
            case 'copy-bbcode':       copyAsBBCode();      break;
            case 'copy-selected-text':copySelectedText();break;
            case 'open-new-tab':      openNewTab();      break;
            case 'copy-page-url':   copyPageUrl();       break;
            case 'view-source':       viewSource();      break;
      }

      hideMenu();
    }

    // ========== 功能函数 ==========

    function getLinkText(link) {
      let text = link.innerText || link.textContent || '';
      text = text.replace(/\s+/g, ' ').trim();

      if (!text) {
            text = link.getAttribute('title') ||
                link.getAttribute('aria-label') ||
                link.querySelector('img')?.getAttribute('alt') || '';
      }

      return text;
    }

    function copyLinkText() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const text = getLinkText(currentLink);
      if (text) {
            GM_setClipboard(text);
            showNotification('复制成功', '已复制链接文字');
      } else {
            showNotification('错误', '链接没有文字内容', 'error');
      }
    }

    function copyLinkUrl() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const url = currentLink.href;
      if (url) {
            GM_setClipboard(url);
            showNotification('复制成功', '已复制链接地址');
      } else {
            showNotification('错误', '链接没有地址', 'error');
      }
    }

    function copyTextAndUrl() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const text = getLinkText(currentLink);
      const url = currentLink.href;
      if (text && url) {
            GM_setClipboard(`${text} ${url}`);
            showNotification('复制成功', '已复制文字和链接');
      } else {
            showNotification('错误', '无法复制文字和链接', 'error');
      }
    }

    function copyAsMarkdown() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const text = getLinkText(currentLink);
      const url = currentLink.href;
      if (text && url) {
            GM_setClipboard(`[${text}](${url})`);
            showNotification('复制成功', '已复制为 Markdown 格式');
      } else {
            showNotification('错误', '无法生成 Markdown 格式', 'error');
      }
    }

    function copyAsHtml() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const text = getLinkText(currentLink);
      const url = currentLink.href;
      if (text && url) {
            GM_setClipboard(`<a href="${url}">${text}</a>`);
            showNotification('复制成功', '已复制为 HTML 格式');
      } else {
            showNotification('错误', '无法生成 HTML 格式', 'error');
      }
    }

    function copyAsBBCode() {
      if (!currentLink) { showNotification('错误', '没有找到链接', 'error'); return; }
      const text = getLinkText(currentLink);
      const url = currentLink.href;
      if (text && url) {
            GM_setClipboard(`${text}`);
            showNotification('复制成功', '已复制为 BBCode 格式');
      } else {
            showNotification('错误', '无法生成 BBCode 格式', 'error');
      }
    }

    function copySelectedText() {
      if (selectedText) {
            GM_setClipboard(selectedText);
            showNotification('复制成功', '已复制选中文字');
      } else {
            showNotification('错误', '没有选中文字', 'error');
      }
    }

    function openNewTab() {
      if (currentLink) {
            GM_openInTab(currentLink.href, { active: true });
            showNotification('新标签页', '正在打开链接...');
      } else {
            showNotification('错误', '没有找到链接', 'error');
      }
    }

    function copyPageUrl() {
      GM_setClipboard(window.location.href);
      showNotification('复制成功', '已复制页面地址');
    }

    function viewSource() {
      GM_openInTab(`view-source:${window.location.href}`, { active: true });
      showNotification('源代码', '正在打开源代码页面...');
    }

    // 显示通知
    // fix 10: 多条通知垂直堆叠,避免互相遮挡
    function showNotification(title, message, type = 'success') {
      const oldNotifications = document.querySelectorAll('.menu-notification');
      oldNotifications.forEach(n => {
            n.classList.remove('show');
            setTimeout(() => n.parentNode && n.parentNode.removeChild(n), 200);
      });

      const notification = document.createElement('div');
      notification.className = `menu-notification notification-${type}`;
      notification.style.fontFamily = DESIGN.fontFamily;

      const icon = type === 'success' ? '✓' : '✗';

      // fix 11: 通知内容全部用 textContent,无注入风险
      const iconEl = document.createElement('div');
      iconEl.className = 'notification-icon';
      iconEl.textContent = icon;

      const contentEl = document.createElement('div');
      contentEl.className = 'notification-content';

      const titleEl = document.createElement('div');
      titleEl.className = 'notification-title';
      titleEl.textContent = title;

      const msgEl = document.createElement('div');
      msgEl.className = 'notification-message';
      msgEl.textContent = message;

      contentEl.appendChild(titleEl);
      contentEl.appendChild(msgEl);
      notification.appendChild(iconEl);
      notification.appendChild(contentEl);

      document.body.appendChild(notification);

      requestAnimationFrame(() => {
            requestAnimationFrame(() => notification.classList.add('show'));
      });

      const dismiss = () => {
            notification.classList.remove('show');
            setTimeout(() => notification.parentNode && notification.parentNode.removeChild(notification), 200);
      };

      setTimeout(dismiss, 1800);
      notification.addEventListener('click', dismiss);
    }

    // 初始化脚本
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      init();
    }
})();

MRBANK 发表于 2026-2-7 23:30

xouou 发表于 2026-2-7 22:55
没有反应

链接!!只支持在链接上右键显示

xouou 发表于 2026-2-7 22:55



没有反应

天才V1 发表于 2026-2-7 22:26

禁止复制和粘贴那种能直接复制么?比如12315这种

WXJYXLWMH 发表于 2026-2-7 22:42

功能很好 支持原创 谢谢

Nine01 发表于 2026-2-8 00:04

感谢分享

愿他们彼此珍惜 发表于 2026-2-8 00:25

感谢分享,试了一下,挺好用的

星凯 发表于 2026-2-8 00:32

好东西啊 铁子。学习了,拿去用了

penz 发表于 2026-2-8 03:15

好东西,感谢分享

feng_129 发表于 2026-2-8 05:06


1.背景颜色,有些浏览器不清楚
2.有时候或存在多个$$$$$$$$$$$$$$$
页: [1] 2 3 4 5 6
查看完整版本: 【油猴脚本】右键链接增强 - 快速复制链接文字、地址及多种格式