11ze / 查看网址

// ==UserScript==
// @name         查看网址
// @namespace    https://github.com/11ze
// @version      0.1.14
// @description  2025-12-25
// @author       11ze
// @license      MIT
// @match        *://*/*
// @noframes
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItc2VhcmNoIj48Y2lyY2xlIGN4PSIxMSIgY3k9IjExIiByPSI4Ij48L2NpcmNsZT48cGF0aCBkPSJtMjEgMjEtNC4zNS00LjM1Ij48L3BhdGg+PC9zdmc+
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  'use strict';

  function parseUrl(url) {
    if (typeof url !== 'string') {
      console.error('parseUrl: 参数必须是字符串');
      return [];
    }
    if (!url) {
      console.error('parseUrl: 参数不能为空');
      return [];
    }

    const multiplePath = url.split('#');
    const result = [];

    for (let i = 0; i < multiplePath.length; i++) {
      const path = multiplePath[i];
      if (!path) continue;

      let host, otherUrl;

      if (i === 0) {
        [host, otherUrl] = path.split('?');
      } else {
        host = path.split('?')[0];
        otherUrl = path.split('?')[1];
      }

      result.push({
        type: 'host',
        value: host,
      });

      if (otherUrl) {
        // 使用 URLSearchParams 解析参数
        const params = new URLSearchParams(otherUrl);
        const paramEntries = Array.from(params.entries());

        if (paramEntries.length > 0) {
          result.push({
            type: 'table',
          });
        }

        for (const [key, value] of paramEntries) {
          result.push({
            type: 'param',
            key: key,
            value: value || '',
          });
        }
      }
    }

    return result;
  }

  function main() {
    const urlInfo = parseUrl(decodeURIComponent(window.location.href));

    console.log('url-reader: urlInfo');
    console.log(urlInfo);

    const popupId = '11ze-url-reader-popup';

    const popup = document.createElement('div');
    popup.className = 'popup';
    popup.id = popupId;
    popup.classList.add('url-reader-popup');

    // 设置弹窗样式
    popup.style.position = 'fixed';
    popup.style.top = '200px';
    popup.style.right = '400px';
    popup.style.zIndex = '9999';
    popup.style.backgroundColor = '#ffffff';
    popup.style.padding = '20px';
    popup.style['max-height'] = '80vh';
    popup.style['overflow-y'] = 'auto';
    popup.style.display = 'flex';
    popup.style.flexDirection = 'column';
    popup.style.width = '450px';
    popup.style.fontSize = '14px';
    popup.style.borderRadius = '12px';
    popup.style.boxShadow = '0 10px 30px rgba(0, 0, 0, 0.15)';
    popup.style.border = '1px solid #e1e8ed';

    let currentHost = '';
    let currentTable = null;
    let paramString = '';
    let hasParams = false;

    // 显示地址栏内容
    const fullUrl = window.location.href;
    const fullUrlDiv = document.createElement('div');
    fullUrlDiv.textContent = decodeURIComponent(fullUrl);
    // 内容显示完整并换行
    fullUrlDiv.style['overflow-wrap'] = 'break-word';
    fullUrlDiv.style.padding = '12px';
    fullUrlDiv.style.backgroundColor = '#f8fafc';
    fullUrlDiv.style.borderRadius = '8px';
    fullUrlDiv.style.border = '1px solid #e2e8f0';
    fullUrlDiv.style.fontSize = '13px';
    fullUrlDiv.style.lineHeight = '1.5';
    fullUrlDiv.style.color = '#1e293b';
    fullUrlDiv.style.marginBottom = '16px';
    popup.appendChild(fullUrlDiv);

    popup.appendChild(createSeparator());

    for (let i = 0; i < urlInfo.length; i++) {
      const param = urlInfo[i];
      const type = param.type;
      if (type === 'host') {
        if (currentTable) {
          if (hasParams) {
            // 为上一个 host 添加参数复制按钮
            addParamsCopyButton(popup, paramString);
            paramString = ''; // 重置参数字符串
          }

          popup.appendChild(createSeparator());
        }

        hasParams = false;

        currentHost = param.value;

        // 创建 host 显示和复制按钮
        const hostDiv = createHostDiv(currentHost);
        popup.appendChild(hostDiv);
      } else if (type === 'param') {
        // 添加参数行到表格
        addParamRow(currentTable, param);
        // 构建参数字符串
        paramString += (paramString ? '&' : '') + `${param.key}=${param.value}`;
        if (paramString) {
          hasParams = true;
        }
      } else if (type === 'table') {
        // 创建新的表格
        currentTable = createTable();
        popup.appendChild(currentTable);
      }
    }

    // 为最后一个 host 添加参数复制按钮
    if (paramString) {
      addParamsCopyButton(popup, paramString);
    }

    const closeButton = document.createElement('button');
    closeButton.textContent = '关闭';
    closeButton.style.padding = '10px 16px';
    closeButton.style.cursor = 'pointer';
    closeButton.style.fontSize = '13px';
    closeButton.style.background = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
    closeButton.style.border = 'none';
    closeButton.style.borderRadius = '6px';
    closeButton.style.color = 'white';
    closeButton.style.width = '100%';
    closeButton.style.fontWeight = '600';
    closeButton.style.transition = 'all 0.2s ease';
    closeButton.style.boxShadow = '0 2px 4px rgba(239, 68, 68, 0.3)';
    closeButton.style.marginTop = '8px';

    closeButton.onclick = () => {
      popup.remove();
    };

    // 添加悬停效果
    closeButton.onmouseover = () => {
      closeButton.style.transform = 'translateY(-1px)';
      closeButton.style.boxShadow = '0 4px 8px rgba(239, 68, 68, 0.4)';
    };
    closeButton.onmouseout = () => {
      closeButton.style.transform = 'translateY(0)';
      closeButton.style.boxShadow = '0 2px 4px rgba(239, 68, 68, 0.3)';
    };

    popup.appendChild(closeButton);

    document.body.appendChild(popup);

    // 添加点击事件监听器到 document
    const closePopup = (event) => {
      if (!popup.contains(event.target) && event.target.id !== 'url-reader-menu-item') {
        popup.remove();
        document.removeEventListener('click', closePopup);
      }
    };
    document.addEventListener('click', closePopup);
  }

  function createSeparator() {
    const separator = document.createElement('div');
    separator.style.height = '1px';
    separator.style.backgroundColor = '#e2e8f0';
    separator.style.margin = '16px 0';
    separator.style.width = '100%';
    return separator;
  }

  function createHostDiv(host) {
    const hostDiv = document.createElement('div');
    hostDiv.style.display = 'flex';
    hostDiv.style.marginBottom = '12px';
    hostDiv.style.padding = '10px 12px';
    hostDiv.style.backgroundColor = '#f1f5f9';
    hostDiv.style.borderRadius = '6px';
    hostDiv.style.borderLeft = '4px solid #3b82f6';

    const hostSpan = document.createElement('span');
    hostSpan.textContent = host;
    hostSpan.style.textAlign = 'left';
    hostSpan.style.fontSize = '14px';
    hostSpan.style.fontWeight = '600';
    hostSpan.style.color = '#000';
    hostDiv.appendChild(hostSpan);

    return hostDiv;
  }

  function createTable() {
    const table = document.createElement('table');
    table.className = 'table';
    table.style.minWidth = '400px';
    table.style.border = '1px solid #e2e8f0';
    table.style.borderRadius = '8px';
    table.style.textAlign = 'left';
    table.style.fontSize = '13px';
    table.style.borderCollapse = 'separate';
    table.style.borderSpacing = '0';
    table.style.overflow = 'hidden';

    table.innerHTML = `
      <thead>
        <tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
          <th style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: white; border: none;">参数名</th>
          <th style="padding: 12px 16px; font-size: 13px; font-weight: 600; color: white; border: none;">参数值</th>
        </tr>
      </thead>
      <tbody></tbody>
    `;

    return table;
  }

  function addParamRow(table, param) {
    const row = table.insertRow();
    const cell1 = row.insertCell(0);
    const cell2 = row.insertCell(1);

    // 创建参数名元素
    const keySpan = document.createElement('span');
    keySpan.textContent = param.key;
    keySpan.style.cursor = 'pointer';
    keySpan.style.padding = '4px 8px';
    keySpan.style.borderRadius = '4px';
    keySpan.style.transition = 'all 0.2s ease';
    keySpan.title = '点击复制参数名';

    // 创建参数值元素
    const valueSpan = document.createElement('span');
    valueSpan.textContent = param.value;
    valueSpan.style.cursor = 'pointer';
    valueSpan.style.padding = '4px 8px';
    valueSpan.style.borderRadius = '4px';
    valueSpan.style.transition = 'all 0.2s ease';
    valueSpan.title = '点击复制参数值';

    // 添加点击复制功能到单元格
    cell1.onclick = () => {
      copyToClipboard(param.key, keySpan, '已复制');
    };

    cell2.onclick = () => {
      copyToClipboard(param.value, valueSpan, '已复制');
    };

    // 添加悬停效果到单元格
    cell1.onmouseover = () => {
      cell1.style.backgroundColor = '#e0f2fe';
      keySpan.style.color = '#0369a1';
    };
    cell1.onmouseout = () => {
      cell1.style.backgroundColor = '';
      keySpan.style.color = '';
    };

    cell2.onmouseover = () => {
      cell2.style.backgroundColor = '#e0f2fe';
      valueSpan.style.color = '#0369a1';
    };
    cell2.onmouseout = () => {
      cell2.style.backgroundColor = '';
      valueSpan.style.color = '';
    };

    // 设置单元格样式,使其可点击
    cell1.style.cursor = 'pointer';
    cell2.style.cursor = 'pointer';

    cell1.appendChild(keySpan);
    cell2.appendChild(valueSpan);

    cell1.style.padding = '12px 16px';
    cell2.style.padding = '12px 16px';
    cell1.style.borderBottom = '1px solid #f1f5f9';
    cell2.style.borderBottom = '1px solid #f1f5f9';
    cell1.style.fontSize = '13px';
    cell2.style.fontSize = '13px';
    cell1.style.color = '#374151';
    cell2.style.color = '#374151';
    cell1.style.fontWeight = '500';

    // 添加行悬停效果
    row.style.transition = 'background-color 0.2s ease';
    row.onmouseover = () => (row.style.backgroundColor = '#f8fafc');
    row.onmouseout = () => (row.style.backgroundColor = '');
  }

  function addParamsCopyButton(popup, paramString) {
    const paramsDiv = document.createElement('div');
    paramsDiv.style.display = 'flex';
    paramsDiv.style.marginBottom = '16px';
    paramsDiv.style.width = '100%';

    const copyParamsButton = document.createElement('button');
    copyParamsButton.textContent = '复制参数';
    copyParamsButton.style.padding = '8px 16px';
    copyParamsButton.style.cursor = 'pointer';
    copyParamsButton.style.fontSize = '13px';
    copyParamsButton.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)';
    copyParamsButton.style.border = 'none';
    copyParamsButton.style.borderRadius = '6px';
    copyParamsButton.style.color = 'white';
    copyParamsButton.style.width = '100%';
    copyParamsButton.style.fontWeight = '500';
    copyParamsButton.style.transition = 'all 0.2s ease';
    copyParamsButton.style.boxShadow = '0 2px 4px rgba(59, 130, 246, 0.3)';

    // 添加悬停效果
    copyParamsButton.onmouseover = () => {
      copyParamsButton.style.transform = 'translateY(-1px)';
      copyParamsButton.style.boxShadow = '0 4px 8px rgba(59, 130, 246, 0.4)';
    };
    copyParamsButton.onmouseout = () => {
      copyParamsButton.style.transform = 'translateY(0)';
      copyParamsButton.style.boxShadow = '0 2px 4px rgba(59, 130, 246, 0.3)';
    };

    copyParamsButton.onclick = () => copyToClipboard(paramString, copyParamsButton, '已复制');
    paramsDiv.appendChild(copyParamsButton);

    popup.appendChild(paramsDiv);
  }

  function copyToClipboard(text, button, successMessage) {
    // 防止重复点击
    if (button.disabled) {
      return;
    }

    // 保存原始样式和文本
    const originalText = button.textContent;
    const originalBackground = button.style.background;
    const originalBoxShadow = button.style.boxShadow;

    // 立即禁用按钮,防止重复点击
    button.disabled = true;

    const copyTextToClipboard = (text) => {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        return navigator.clipboard.writeText(text);
      } else {
        // 回退方法:创建一个临时的文本区域元素
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed'; // 避免滚动到底部
        textArea.style.opacity = '0';
        textArea.style.pointerEvents = 'none';
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();
        try {
          return document.execCommand('copy')
            ? Promise.resolve()
            : Promise.reject(new Error('复制失败'));
        } catch (err) {
          return Promise.reject(err);
        } finally {
          document.body.removeChild(textArea);
        }
      }
    };

    copyTextToClipboard(text)
      .then(() => {
        // 立即更新按钮状态
        button.textContent = successMessage;
        button.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
        button.style.boxShadow = '0 2px 4px rgba(16, 185, 129, 0.3)';

        // 1秒后恢复原始状态
        setTimeout(() => {
          button.textContent = originalText;
          button.disabled = false;
          button.style.background = originalBackground;
          button.style.boxShadow = originalBoxShadow;
        }, 1000);
      })
      .catch((err) => {
        console.error('复制失败:', err);
        button.style.background = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
        button.textContent = '复制失败';

        // 1.5秒后恢复原始状态
        setTimeout(() => {
          button.textContent = originalText;
          button.disabled = false;
          button.style.background = originalBackground;
          button.style.boxShadow = originalBoxShadow;
        }, 1500);
      });
  }

  GM_registerMenuCommand('查看', main);
})();