中燃WMS二维码生成器

获取页面信息并生成可拖拽的悬浮二维码

// ==UserScript==
// @name         中燃WMS二维码生成器
// @namespace    https://wms.chinagasholdings.com
// @version      1.0
// @description  获取页面信息并生成可拖拽的悬浮二维码
// @author       intpfx
// @match        https://wms.chinagasholdings.com/logincenter/wms_workplace
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // 全局变量
  let isDragging = false;
  let offsetX, offsetY;
  let qrcodeLib = null; // 存储QRCode库的引用
  let isMinimized = false; // 记录是否处于最小化状态
  let fontAwesomeLoaded = false; // 记录font-awesome是否已加载

  // 【关键】判断哈希值是否匹配目标 #A3001
  function isTargetUrl() {
    return window.location.hash === '#A3001';
  }

  // 动态导入font-awesome
  async function loadFontAwesome() {
    if (fontAwesomeLoaded) return;

    try {
      // 检查是否已加载
      if (document.querySelector('link[href*="font-awesome.min.css"]')) {
        fontAwesomeLoaded = true;
        return;
      }

      // 使用esm.sh导入font-awesome
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = 'https://esm.sh/[email protected]/css/font-awesome.min.css';
      
      // 返回Promise等待加载完成
      return new Promise((resolve, reject) => {
        link.onload = () => {
          fontAwesomeLoaded = true;
          resolve();
        };
        link.onerror = (error) => {
          console.error('font-awesome加载失败:', error);
          reject(new Error('图标库加载失败'));
        };
        document.head.appendChild(link);
      });
    } catch (error) {
      console.error('加载font-awesome时出错:', error);
      throw error;
    }
  }

  // 导入QRCode库
  async function importQRCode() {
    if (qrcodeLib) return qrcodeLib;
    
    try {
      const { qrcode } = await import('https://esm.sh/jsr/@libs/qrcode');
      qrcodeLib = qrcode;
      return qrcode;
    } catch (error) {
      console.error('JSR包导入失败:', error);
      showNotification('二维码库加载失败,请检查网络', 'error');
      throw error;
    }
  }

  // 显示通知
  function showNotification(message, type = 'info') {
    // 检查是否已有通知
    let notification = document.getElementById('qrcode-notification');
    if (notification) {
      notification.remove();
    }

    notification = document.createElement('div');
    notification.id = 'qrcode-notification';
    notification.className = `qrcode-notification ${type}`;
    notification.textContent = message;
    document.body.appendChild(notification);

    // 3秒后自动消失
    setTimeout(() => {
      notification.style.opacity = '0';
      setTimeout(() => notification.remove(), 300);
    }, 3000);
  }

  // 创建悬浮窗和通知样式
  function createStyles() {
    GM_addStyle(`
            #qrcode-floating-window {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: white;
                border: 2px solid #3498db;
                border-radius: 10px;
                padding: 15px;
                box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
                z-index: 999999;
                cursor: move;
                transition: all 0.3s ease;
                min-width: 250px;
                display: flex;
                flex-direction: column;
                align-items: center;
            }
            
            #qrcode-floating-window:hover {
                box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
                border-color: #2980b9;
            }
            
            #qrcode-header {
                width: 100%;
                display: flex;
                justify-content: flex-end;
                gap: 8px;
                margin-bottom: 10px;
            }
            
            .qrcode-btn {
                background: #f1f5f9;
                color: #3498db;
                border: none;
                border-radius: 5px;
                width: 30px;
                height: 30px;
                font-size: 14px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s;
            }
            
            .qrcode-btn:hover {
                background: #3498db;
                color: white;
            }
            
            #qrcode-container {
                display: flex;
                justify-content: center;
                margin-bottom: 15px;
                width: 200px;
                height: 200px;
            }
            
            #qrcode-info {
                font-size: 14px;
                color: #333;
                text-align: center;
                line-height: 1.6;
                max-height: 100px;
                overflow-y: auto;
                width: 100%;
            }
            
            .qrcode-notification {
                position: fixed;
                bottom: 20px;
                right: 20px;
                padding: 10px 15px;
                border-radius: 5px;
                color: white;
                z-index: 999999;
                transition: opacity 0.3s;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            }
            
            .qrcode-notification.info {
                background: #3498db;
            }
            
            .qrcode-notification.success {
                background: #2ecc71;
            }
            
            .qrcode-notification.error {
                background: #e74c3c;
            }
            
            /* 最小化样式 */
            #qrcode-floating-window.minimized {
                min-width: auto;
                padding: 8px;
            }
            
            #qrcode-floating-window.minimized #qrcode-container,
            #qrcode-floating-window.minimized #qrcode-info {
                display: none;
            }
            
            .error-message {
                color: #e74c3c;
                text-align: center;
                padding: 20px;
                width: 100%;
                box-sizing: border-box;
            }
        `);
  }

  // 获取页面信息
  function getPageInfo() {
    try {
      // 找到页面中id为A3001detailFormMain的表单
      const form = document.querySelector('#A3001detailFormMain');
      
      if (!form) {
        throw new Error('未找到表单元素A3001detailFormMain');
      }
      
      // 封装获取输入值的函数,增加错误处理
      const getInputValue = (name, alias) => {
        const input = form.querySelector(`input[name="${name}"]`);
        if (!input) {
          console.warn(`未找到名为${name}的输入框`);
          return `无${alias}信息`;
        }
        return input.value || `无${alias}信息`;
      };
      
      return {
        supplierName: getInputValue('lotAtt04','供应商名称'),
        sku: getInputValue('sku','物料编码'),
        dop: getInputValue('lotAtt01','生产日期'),
        batch: getInputValue('lotAtt05','生产批次'),
      };
    } catch (error) {
      console.error('获取页面信息失败:', error);
      showNotification('获取信息失败: ' + error.message, 'error');
      // 返回默认值,确保功能可以继续运行
      return {
        supplierName: '暂无数据',
        sku: '暂无数据',
        dop: '暂无数据',
        batch: '暂无数据',
      };
    }
  }

  // 生成二维码内容
  function generateQRContent(pageInfo) {
    return `供应商名称: ${pageInfo.supplierName}\n物料编码: ${pageInfo.sku}\n生产日期: ${pageInfo.dop}\n生产批次: ${pageInfo.batch}`;
  }

  // 创建悬浮窗
  function createFloatingWindow() {
    // 检查是否已有悬浮窗
    const existingWindow = document.getElementById('qrcode-floating-window');
    if (existingWindow) {
      existingWindow.remove();
    }

    const floatingWindow = document.createElement('div');
    floatingWindow.id = 'qrcode-floating-window';

    // 尝试从存储中获取位置
    const savedPosition = GM_getValue('qrcodeWindowPosition');
    if (savedPosition) {
      floatingWindow.style.left = savedPosition.left;
      floatingWindow.style.top = savedPosition.top;
      floatingWindow.style.transform = 'none';
    }

    // 按钮头部容器
    const header = document.createElement('div');
    header.id = 'qrcode-header';
    floatingWindow.appendChild(header);

    // 刷新按钮(使用图标)
    const refreshBtn = document.createElement('button');
    refreshBtn.className = 'qrcode-btn';
    refreshBtn.innerHTML = '<i class="fa fa-refresh"></i>';
    refreshBtn.title = '刷新二维码';
    refreshBtn.addEventListener('click', async (e) => {
      e.stopPropagation(); // 防止触发拖拽
      refreshBtn.disabled = true;
      refreshBtn.innerHTML = '<i class="fa fa-circle-o-notch fa-spin"></i>';
      try {
        await updateQRCode();
        showNotification('二维码已更新', 'success');
      } catch (error) {
        showNotification('更新失败', 'error');
      } finally {
        refreshBtn.disabled = false;
        refreshBtn.innerHTML = '<i class="fa fa-refresh"></i>';
      }
    });
    header.appendChild(refreshBtn);

    // 缩小按钮(使用图标)
    const minimizeBtn = document.createElement('button');
    minimizeBtn.className = 'qrcode-btn';
    minimizeBtn.innerHTML = '<i class="fa fa-compress"></i>';
    minimizeBtn.title = '最小化';
    minimizeBtn.addEventListener('click', (e) => {
      e.stopPropagation(); // 防止触发拖拽
      isMinimized = !isMinimized;
      if (isMinimized) {
        floatingWindow.classList.add('minimized');
        minimizeBtn.innerHTML = '<i class="fa fa-expand"></i>';
        minimizeBtn.title = '恢复';
      } else {
        floatingWindow.classList.remove('minimized');
        minimizeBtn.innerHTML = '<i class="fa fa-compress"></i>';
        minimizeBtn.title = '最小化';
      }
    });
    header.appendChild(minimizeBtn);

    // 二维码容器
    const qrcodeContainer = document.createElement('div');
    qrcodeContainer.id = 'qrcode-container';
    floatingWindow.appendChild(qrcodeContainer);

    // 信息显示
    const infoDiv = document.createElement('div');
    infoDiv.id = 'qrcode-info';
    floatingWindow.appendChild(infoDiv);

    document.body.appendChild(floatingWindow);
    return floatingWindow;
  }

  // 更新二维码
  async function updateQRCode() {
    if (!qrcodeLib) {
      qrcodeLib = await importQRCode();
    }
    
    const pageInfo = getPageInfo();
    const qrContent = generateQRContent(pageInfo);

    // 更新信息显示
    const infoDiv = document.getElementById('qrcode-info');
    if (infoDiv) {
      infoDiv.innerHTML = `
        <div>供应商名称: ${pageInfo.supplierName.length > 20 ? pageInfo.supplierName.substring(0, 20) + '...' : pageInfo.supplierName}</div>
        <div>物料编码: ${pageInfo.sku}</div>
        <div>生产日期: ${pageInfo.dop}</div>
        <div>生产批次: ${pageInfo.batch}</div>
      `;
    }

    // 生成二维码
    const qrcodeContainer = document.getElementById('qrcode-container');
    if (!qrcodeContainer) {
      throw new Error('二维码容器不存在');
    }
    
    // 清除现有内容
    qrcodeContainer.innerHTML = '';
    
    try {
      // 生成SVG格式的二维码
      let svg = qrcodeLib(qrContent, { output: "svg" });
      
      // 验证SVG内容是否有效
      if (typeof svg !== 'string' || svg.trim() === '') {
        throw new Error("生成的SVG内容为空或无效");
      }

      // 将svg字符串里的viewBox更改为"0 0 52 52"
      svg = svg.replace(/viewBox="0 0 \d+ \d+"/, 'viewBox="0 0 52 52"');
      
      // 创建临时容器解析SVG
      const tempContainer = document.createElement('div');
      tempContainer.innerHTML = svg;
      
      // 尝试获取SVG元素
      let svgElement = tempContainer.querySelector('svg');
      
      // 如果直接解析失败,尝试使用DOMParser
      if (!svgElement) {
        const parser = new DOMParser();
        try {
          const doc = parser.parseFromString(svg, "image/svg+xml");
          // 检查解析错误
          const parserError = doc.querySelector('parsererror');
          if (parserError) {
            throw new Error("SVG解析错误: " + parserError.textContent);
          }
          svgElement = doc.documentElement;
        } catch (parseError) {
          console.error('DOMParser解析失败:', parseError);
          throw new Error("无法解析SVG内容");
        }
      }
      
      if (svgElement && svgElement.tagName.toLowerCase() === 'svg') {
        // 清除可能的事件监听器和不需要的属性
        svgElement.removeAttribute('onload');
        svgElement.removeAttribute('onerror');
        
        // 添加到容器
        qrcodeContainer.appendChild(svgElement);
      } else {
        throw new Error("生成的内容不是有效的SVG元素");
      }
    } catch (error) {
      console.error('生成二维码失败:', error);
      // 创建错误信息元素
      const errorDiv = document.createElement('div');
      errorDiv.className = 'error-message';
      errorDiv.textContent = '二维码生成失败';
      qrcodeContainer.appendChild(errorDiv);
      throw error;
    }
  }

  // 实现拖拽功能
  function enableDragging(element) {
    element.addEventListener('mousedown', (e) => {
      // 只有点击非按钮区域才允许拖拽
      if (e.target.closest('.qrcode-btn')) {
        return;
      }

      isDragging = true;
      const rect = element.getBoundingClientRect();
      offsetX = e.clientX - rect.left;
      offsetY = e.clientY - rect.top;
      element.style.cursor = 'grabbing';
      element.style.transition = 'none';
    });

    document.addEventListener('mousemove', (e) => {
      if (!isDragging) return;

      const x = e.clientX - offsetX;
      const y = e.clientY - offsetY;

      // 限制在窗口内
      const maxX = window.innerWidth - element.offsetWidth;
      const maxY = window.innerHeight - element.offsetHeight;

      const clampedX = Math.max(0, Math.min(x, maxX));
      const clampedY = Math.max(0, Math.min(y, maxY));

      element.style.left = clampedX + 'px';
      element.style.top = clampedY + 'px';
      element.style.transform = 'none';
    });

    document.addEventListener('mouseup', () => {
      if (isDragging) {
        isDragging = false;
        element.style.cursor = 'move';
        element.style.transition = 'all 0.3s ease';

        // 保存位置到存储
        GM_setValue('qrcodeWindowPosition', {
          left: element.style.left,
          top: element.style.top
        });

        // 自动吸附到最近的边缘
        const rect = element.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;

        // 计算距离各边缘的距离
        const distances = {
          left: centerX,
          right: windowWidth - centerX,
          top: centerY,
          bottom: windowHeight - centerY
        };

        // 找到最近的边缘
        const closestEdge = Object.keys(distances).reduce((a, b) =>
          distances[a] < distances[b] ? a : b
        );

        // 吸附到边缘
        switch (closestEdge) {
          case 'left':
            element.style.left = '10px';
            break;
          case 'right':
            element.style.left = (windowWidth - rect.width - 10) + 'px';
            break;
          case 'top':
            element.style.top = '10px';
            break;
          case 'bottom':
            element.style.top = (windowHeight - rect.height - 10) + 'px';
            break;
        }
      }
    });

    // 防止拖拽时选择文本
    element.addEventListener('selectstart', (e) => {
      if (isDragging) {
        e.preventDefault();
      }
    });

    // 触摸设备支持
    element.addEventListener('touchstart', (e) => {
      if (e.touches.length !== 1) return;
      
      const touch = e.touches[0];
      // 只有点击非按钮区域才允许拖拽
      if (e.target.closest('.qrcode-btn')) {
        return;
      }

      isDragging = true;
      const rect = element.getBoundingClientRect();
      offsetX = touch.clientX - rect.left;
      offsetY = touch.clientY - rect.top;
      element.style.cursor = 'grabbing';
      element.style.transition = 'none';
      e.preventDefault();
    });

    document.addEventListener('touchmove', (e) => {
      if (!isDragging || e.touches.length !== 1) return;

      const touch = e.touches[0];
      const x = touch.clientX - offsetX;
      const y = touch.clientY - offsetY;

      // 限制在窗口内
      const maxX = window.innerWidth - element.offsetWidth;
      const maxY = window.innerHeight - element.offsetHeight;

      const clampedX = Math.max(0, Math.min(x, maxX));
      const clampedY = Math.max(0, Math.min(y, maxY));

      element.style.left = clampedX + 'px';
      element.style.top = clampedY + 'px';
      element.style.transform = 'none';
      e.preventDefault();
    });

    document.addEventListener('touchend', () => {
      if (isDragging) {
        isDragging = false;
        element.style.cursor = 'move';
        element.style.transition = 'all 0.3s ease';

        // 保存位置到存储
        GM_setValue('qrcodeWindowPosition', {
          left: element.style.left,
          top: element.style.top
        });

        // 自动吸附到最近的边缘
        const rect = element.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;

        // 计算距离各边缘的距离
        const distances = {
          left: centerX,
          right: windowWidth - centerX,
          top: centerY,
          bottom: windowHeight - centerY
        };

        // 找到最近的边缘
        const closestEdge = Object.keys(distances).reduce((a, b) =>
          distances[a] < distances[b] ? a : b
        );

        // 吸附到边缘
        switch (closestEdge) {
          case 'left':
            element.style.left = '10px';
            break;
          case 'right':
            element.style.left = (windowWidth - rect.width - 10) + 'px';
            break;
          case 'top':
            element.style.top = '10px';
            break;
          case 'bottom':
            element.style.top = (windowHeight - rect.height - 10) + 'px';
            break;
        }
      }
    });
  }

  // 预加载字体图标和二维码库
  async function preloadResources() {
    await loadFontAwesome();
    await importQRCode();
  }

  // 初始化函数
  async function initQRCodeGenerator() {
    // 先判断哈希值,不匹配则直接退出,不执行后续逻辑
    if (!isTargetUrl()) {
      console.log('当前页面哈希值不匹配,不加载二维码生成器');
      return;
    }

    try {
      // 先加载字体图标
      await loadFontAwesome();
      
      // 导入二维码库
      await importQRCode();

      // 创建样式
      createStyles();

      // 创建悬浮窗
      const floatingWindow = createFloatingWindow();
      if (!floatingWindow) {
        throw new Error('无法创建悬浮窗口');
      }

      // 启用拖拽
      enableDragging(floatingWindow);

      // 生成初始二维码
      await updateQRCode();

      console.log('页面信息二维码生成器已启动!');
      showNotification('二维码生成器已启动', 'success');
    } catch (error) {
      console.error('初始化失败:', error);
      // 图标加载失败时仍然显示通知,只是没有图标
      showNotification('二维码生成器初始化失败,请刷新页面重试', 'error');
    }
  }

  // 页面加载完成后初始化
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    preloadResources();
  } else {
    document.addEventListener('DOMContentLoaded', preloadResources);
  }

  // 监听hash变化(如单页应用)
  window.addEventListener('hashchange', () => {
    const floatingWindow = document.getElementById('qrcode-floating-window');
    if (isTargetUrl()) {
      // 切换到目标hash,初始化生成器
      if (!floatingWindow) initQRCodeGenerator();
    } else {
      // 离开目标hash,移除生成器
      if (floatingWindow) {
        floatingWindow.remove();
        // 清除通知
        const notification = document.getElementById('qrcode-notification');
        if (notification) notification.remove();
      }
    }
  });

})();