菜单

Administrator
发布于 2025-09-26 / 2 阅读
0
0

一个简单的二维码生成器web页面+源码

源码在最后

256
0%
15%

源码

<!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
  <style>
    .qr-card {
      max-width: 720px;
      margin: 1.5rem auto;
      padding: 1rem;
      border: 1px solid #e5e7eb;
      border-radius: 16px;
      background: #fff;
      box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
    }

    .qr-row {
      display: flex;
      flex-wrap: wrap;
      gap: .75rem;
      align-items: center;
      margin: .5rem 0
    }

    .qr-row label {
      font-size: .9rem;
      color: #374151
    }

    .qr-row input[type="text"] {
      flex: 1 1 320px;
      padding: .6rem .8rem;
      border: 1px solid #d1d5db;
      border-radius: 10px
    }

    .qr-row input[type="number"] {
      width: 5.5rem;
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px
    }

    .qr-row select,
    .qr-row input[type="color"] {
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px;
      background: #fff
    }

    .qr-actions {
      display: flex;
      gap: .6rem;
      flex-wrap: wrap;
      margin-top: .75rem
    }

    .qr-btn {
      appearance: none;
      border: 1px solid #111827;
      background: #111827;
      color: #fff;
      padding: .55rem .9rem;
      border-radius: 999px;
      cursor: pointer
    }

    .qr-btn.alt {
      border-color: #d1d5db;
      background: #fff;
      color: #111827
    }

    .qr-preview {
      display: grid;
      place-items: center;
      background: #f9fafb;
      border-radius: 12px;
      padding: 1rem;
      margin-top: .75rem
    }

    .qr-size-output {
      min-width: 3ch;
      text-align: right;
      font-variant-numeric: tabular-nums
    }

    .qr-row input[type="checkbox"] {
      width: auto;
      margin: 0 0.5rem;
    }

    .qr-row input[type="file"] {
      font-size: 0.8rem;
      padding: 0.3rem;
    }

    /* 二维码艺术效果样式 */
    .qr-shadow {
      filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
    }

    .qr-glow {
      filter: drop-shadow(0 0 10px currentColor);
    }

    .qr-3d {
      filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
    }

    .qr-neon {
      filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
    }

    /* 动画效果 */
    .qr-pulse {
      animation: qr-pulse 2s ease-in-out infinite;
    }

    .qr-rotate {
      animation: qr-rotate 4s linear infinite;
    }

    .qr-fade {
      animation: qr-fade 3s ease-in-out infinite;
    }

    @keyframes qr-pulse {
      0%, 100% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.05); opacity: 0.8; }
    }

    @keyframes qr-rotate {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    @keyframes qr-fade {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.6; }
    }

    /* 链接测试模态框 */
    .qr-modal {
      display: none;
      position: fixed;
      z-index: 1000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
    }

    .qr-modal-content {
      background-color: #fefefe;
      margin: 15% auto;
      padding: 20px;
      border: none;
      border-radius: 12px;
      width: 300px;
      text-align: center;
    }

    .qr-modal-close {
      color: #aaa;
      float: right;
      font-size: 28px;
      font-weight: bold;
      cursor: pointer;
    }

    @media (prefers-color-scheme: dark) {
      .qr-card {
        background: #111827;
        border-color: #374151;
        color: #e5e7eb
      }

      .qr-row input,
      .qr-row select {
        background: #0b1220;
        color: #e5e7eb;
        border-color: #374151
      }

      .qr-preview {
        background: #0b1220
      }

      .qr-btn.alt {
        background: transparent;
        color: #e5e7eb;
        border-color: #4b5563
      }

      .qr-modal-content {
        background-color: #1f2937;
        color: #e5e7eb;
      }

      .qr-modal-close {
        color: #9ca3af;
      }
    }
  </style>

  <div class="qr-row">
    <label>内容:</label>
    <input id="qr-text" type="text" placeholder="要编码的文本/链接" />
    <button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
  </div>

  <div class="qr-row">
    <label>纠错:</label>
    <select id="qr-level">
      <option value="L">L(约7%)</option>
      <option value="M" selected>M(约15%)</option>
      <option value="Q">Q(约25%)</option>
      <option value="H">H(约30%)</option>
    </select>

    <label>尺寸(px):</label>
    <input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
    <span class="qr-size-output" id="qr-size-out">256</span>

    <label>边距(模块):</label>
    <input id="qr-margin" type="number" min="0" max="8" value="4" />
  </div>

  <div class="qr-row">
    <label>样式模式:</label>
    <select id="qr-style-mode">
      <option value="classic">经典方块</option>
      <option value="rounded">圆角方块</option>
      <option value="dots">圆点</option>
      <option value="artistic">艺术风格</option>
    </select>
    
    <label>圆角程度:</label>
    <input id="qr-border-radius" type="range" min="0" max="50" value="0" />
    <span class="qr-size-output" id="qr-radius-out">0%</span>
  </div>

  <div class="qr-row">
    <label>颜色模式:</label>
    <select id="qr-color-mode">
      <option value="solid">单色</option>
      <option value="gradient">线性渐变</option>
      <option value="radial">径向渐变</option>
      <option value="rainbow">彩虹渐变</option>
    </select>
    
    <label>渐变方向:</label>
    <select id="qr-gradient-direction">
      <option value="0">水平→</option>
      <option value="90">垂直↓</option>
      <option value="45">对角↘</option>
      <option value="135">对角↙</option>
    </select>
  </div>

  <div class="qr-row">
    <label>前景色:</label>
    <input id="qr-dark" type="color" value="#000000" />
    <label>渐变终点色:</label>
    <input id="qr-dark2" type="color" value="#4f46e5" />
    <button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
  </div>

  <div class="qr-row">
    <label>背景色:</label>
    <input id="qr-light" type="color" value="#ffffff" />
    <label>背景渐变色:</label>
    <input id="qr-light2" type="color" value="#f3f4f6" />
    <button class="qr-btn alt" id="qr-invert" type="button">反色</button>
  </div>

  <div class="qr-row">
    <label>Logo图片:</label>
    <input id="qr-logo" type="file" accept="image/*" />
    <label>Logo大小:</label>
    <input id="qr-logo-size" type="range" min="10" max="30" value="15" />
    <span class="qr-size-output" id="qr-logo-size-out">15%</span>
    <button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
  </div>

  <div class="qr-row">
    <label>艺术效果:</label>
    <select id="qr-art-effect">
      <option value="none">无</option>
      <option value="shadow">阴影</option>
      <option value="glow">发光</option>
      <option value="3d">3D效果</option>
      <option value="neon">霓虹</option>
    </select>
    
    <label>动画效果:</label>
    <select id="qr-animation">
      <option value="none">无动画</option>
      <option value="pulse">脉冲</option>
      <option value="rotate">旋转</option>
      <option value="fade">淡入淡出</option>
    </select>
  </div>

  <div class="qr-row">
    <label>链接自动跳转:</label>
    <input id="qr-auto-redirect" type="checkbox" />
    <label>新窗口打开:</label>
    <input id="qr-new-window" type="checkbox" checked />
    <button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
  </div>

  <div class="qr-actions">
    <button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
    <button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
    <button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
  </div>

  <div class="qr-preview">
    <div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
  </div>
  <canvas id="qr-canvas" style="display:none"></canvas>

  <!-- 链接测试模态框 -->
  <div id="qr-link-modal" class="qr-modal">
    <div class="qr-modal-content">
      <span class="qr-modal-close">&times;</span>
      <h3>链接测试</h3>
      <p id="qr-link-preview"></p>
      <div style="margin-top: 15px;">
        <button class="qr-btn" id="qr-open-link">打开链接</button>
        <button class="qr-btn alt" id="qr-copy-link">复制链接</button>
      </div>
    </div>
  </div>
</div>

<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>

<script>
  // 等待库加载完成
  window.addEventListener('load', () => {
    const $ = (sel) => document.querySelector(sel);
    const textEl = $('#qr-text');
    const levelEl = $('#qr-level');
    const sizeEl = $('#qr-size');
    const sizeOut = $('#qr-size-out');
    const marginEl = $('#qr-margin');
    const darkEl = $('#qr-dark');
    const dark2El = $('#qr-dark2');
    const lightEl = $('#qr-light');
    const light2El = $('#qr-light2');
    const invertEl = $('#qr-invert');
    const svgWrap = $('#qr-svg-wrap');
    const canvas = $('#qr-canvas');
    
    // 新增的控制元素
    const styleModeEl = $('#qr-style-mode');
    const borderRadiusEl = $('#qr-border-radius');
    const radiusOut = $('#qr-radius-out');
    const colorModeEl = $('#qr-color-mode');
    const gradientDirectionEl = $('#qr-gradient-direction');
    const logoEl = $('#qr-logo');
    const logoSizeEl = $('#qr-logo-size');
    const logoSizeOut = $('#qr-logo-size-out');
    const artEffectEl = $('#qr-art-effect');
    const animationEl = $('#qr-animation');
    const autoRedirectEl = $('#qr-auto-redirect');
    const newWindowEl = $('#qr-new-window');
    
    // 模态框元素
    const linkModal = $('#qr-link-modal');
    const linkPreview = $('#qr-link-preview');
    const modalClose = $('.qr-modal-close');
    
    let logoImage = null;

    // Canvas圆角矩形兼容性
    if (!CanvasRenderingContext2D.prototype.roundRect) {
      CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
        this.beginPath();
        this.moveTo(x + radius, y);
        this.lineTo(x + width - radius, y);
        this.quadraticCurveTo(x + width, y, x + width, y + radius);
        this.lineTo(x + width, y + height - radius);
        this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        this.lineTo(x + radius, y + height);
        this.quadraticCurveTo(x, y + height, x, y + height - radius);
        this.lineTo(x, y + radius);
        this.quadraticCurveTo(x, y, x + radius, y);
        this.closePath();
      };
    }

    // 初始值:默认填入当前页 URL
    if (!textEl.value) textEl.value = location.href;

    const debounce = (fn, wait = 160) => {
      let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
    };

    function makeQRMatrix(content, level) {
      const qr = qrcode(0, level); // 0 = 自动尺寸
      qr.addData(content);
      qr.make();
      return qr;
    }

    // 颜色工具函数
    function getRandomColor() {
      return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
    }

    function createGradient(color1, color2, direction, type = 'linear') {
      if (type === 'radial') {
        return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
      } else if (type === 'rainbow') {
        return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
      }
      return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
    }

    function renderSVG(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid',
        gradientDirection = 0,
        artEffect = 'none',
        animation = 'none'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const radius = borderRadius / 100 * 0.4; // 转换为相对单位
      
      let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
      
      // 添加渐变定义
      if (colorMode !== 'solid') {
        svg += '<defs>';
        
        // 前景渐变
        if (colorMode === 'gradient') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</linearGradient>';
        } else if (colorMode === 'radial') {
          svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</radialGradient>';
        } else if (colorMode === 'rainbow') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
          const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
          colors.forEach((color, i) => {
            svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
          });
          svg += '</linearGradient>';
        }
        
        // 背景渐变
        svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
        svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
        svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
        svg += '</linearGradient>';
        
        svg += '</defs>';
      }
      
      // 背景
      const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
      svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
      
      // 前景色
      const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = c + border;
            const y = r + border;
            
            if (styleMode === 'dots') {
              svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
            } else if (styleMode === 'rounded' || radius > 0) {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
            } else if (styleMode === 'artistic') {
              // 艺术风格:随机大小和透明度
              const size = 0.8 + Math.random() * 0.4;
              const opacity = 0.7 + Math.random() * 0.3;
              const offset = (1 - size) / 2;
              svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
            } else {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
            }
          }
        }
      }
      
      svg += `</svg>`;
      return svg;
    }

    function renderCanvas(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const scale = Math.max(1, Math.floor(size / units));
      canvas.width = units * scale;
      canvas.height = units * scale;
      const ctx = canvas.getContext('2d');
      
      // 背景渐变
      if (colorMode !== 'solid') {
        const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        bgGradient.addColorStop(0, light);
        bgGradient.addColorStop(1, light2);
        ctx.fillStyle = bgGradient;
      } else {
        ctx.fillStyle = light;
      }
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      
      // 前景渐变
      let fgStyle = dark;
      if (colorMode === 'gradient') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'radial') {
        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;
        const radius = Math.min(centerX, centerY);
        const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'rainbow') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
        colors.forEach((color, i) => {
          fgGradient.addColorStop(i / (colors.length - 1), color);
        });
        fgStyle = fgGradient;
      }
      
      ctx.fillStyle = fgStyle;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = (c + border) * scale;
            const y = (r + border) * scale;
            
            if (styleMode === 'dots') {
              ctx.beginPath();
              ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
              ctx.fill();
            } else if (styleMode === 'rounded' || borderRadius > 0) {
              const radius = (borderRadius / 100) * scale * 0.4;
              ctx.beginPath();
              ctx.roundRect(x, y, scale, scale, radius);
              ctx.fill();
            } else {
              ctx.fillRect(x, y, scale, scale);
            }
          }
        }
      }
      
      // 添加Logo
      if (logoImage) {
        const logoSize = (logoSizeEl.value / 100) * size;
        const logoX = (canvas.width - logoSize) / 2;
        const logoY = (canvas.height - logoSize) / 2;
        
        // 白色背景圆形
        ctx.fillStyle = 'white';
        ctx.beginPath();
        ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
        ctx.fill();
        
        // 绘制Logo
        ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
      }
    }

    // 辅助函数
    function isValidUrl(string) {
      try {
        new URL(string);
        return true;
      } catch (_) {
        return false;  
      }
    }

    function filenameBase() {
      const raw = (textEl.value || '').trim();
      try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
    }

    // Logo处理
    function handleLogoUpload(event) {
      const file = event.target.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = function(e) {
          const img = new Image();
          img.onload = function() {
            logoImage = img;
            generate();
          };
          img.src = e.target.result;
        };
        reader.readAsDataURL(file);
      }
    }

    // 随机配色
    function randomColors() {
      darkEl.value = getRandomColor();
      dark2El.value = getRandomColor();
      lightEl.value = getRandomColor();
      light2El.value = getRandomColor();
      generate();
    }

    // 清除Logo
    function clearLogo() {
      logoImage = null;
      logoEl.value = '';
      generate();
    }

    // 链接测试
    function testLink() {
      const content = textEl.value.trim();
      if (!content) {
        alert('请先输入内容');
        return;
      }
      
      linkPreview.textContent = content;
      linkModal.style.display = 'block';
    }

    // 打开链接
    function openLink() {
      const content = textEl.value.trim();
      if (isValidUrl(content)) {
        if (newWindowEl.checked) {
          window.open(content, '_blank');
        } else {
          window.location.href = content;
        }
      } else {
        alert('不是有效的URL链接');
      }
      linkModal.style.display = 'none';
    }

    // 复制链接
    async function copyLink() {
      const content = textEl.value.trim();
      try {
        await navigator.clipboard.writeText(content);
        alert('链接已复制到剪贴板');
      } catch {
        const t = document.createElement('textarea');
        t.value = content;
        document.body.appendChild(t);
        t.select();
        document.execCommand('copy');
        document.body.removeChild(t);
        alert('链接已复制到剪贴板');
      }
      linkModal.style.display = 'none';
    }

    const generate = () => {
      const content = (textEl.value || '').trim();
      if (!content) { 
        svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>'; 
        return; 
      }
      
      const level = levelEl.value || 'M';
      const size = parseInt(sizeEl.value, 10) || 256;
      const margin = parseInt(marginEl.value, 10) || 4;
      
      const options = {
        dark: darkEl.value || '#000000',
        dark2: dark2El.value || '#4f46e5',
        light: lightEl.value || '#ffffff',
        light2: light2El.value || '#f3f4f6',
        styleMode: styleModeEl.value || 'classic',
        borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
        colorMode: colorModeEl.value || 'solid',
        gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
        artEffect: artEffectEl.value || 'none',
        animation: animationEl.value || 'none'
      };

      const qr = makeQRMatrix(content, level);
      const svg = renderSVG(qr, size, margin, options);
      
      // 应用艺术效果和动画
      let finalSvg = svg;
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = svg;
      const svgElement = tempDiv.querySelector('svg');
      
      if (options.artEffect !== 'none') {
        svgElement.classList.add(`qr-${options.artEffect}`);
      }
      
      if (options.animation !== 'none') {
        svgElement.classList.add(`qr-${options.animation}`);
      }
      
      svgWrap.innerHTML = tempDiv.innerHTML;
      renderCanvas(qr, size, margin, options);
      
      sizeOut.textContent = size;
      radiusOut.textContent = options.borderRadius + '%';
      logoSizeOut.textContent = logoSizeEl.value + '%';
      
      // 自动跳转功能
      if (autoRedirectEl.checked && isValidUrl(content)) {
        svgWrap.style.cursor = 'pointer';
        svgWrap.title = '点击访问链接';
        svgWrap.onclick = () => {
          if (newWindowEl.checked) {
            window.open(content, '_blank');
          } else {
            window.location.href = content;
          }
        };
      } else {
        svgWrap.style.cursor = 'default';
        svgWrap.title = '';
        svgWrap.onclick = null;
      }
    };

    // 下载 PNG
    const downloadPNG = () => {
      const a = document.createElement('a');
      a.download = `${filenameBase()}.png`;
      a.href = canvas.toDataURL('image/png');
      a.click();
    };

    // 下载 SVG
    const downloadSVG = () => {
      const svgEl = svgWrap.querySelector('svg');
      if (!svgEl) return;
      const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.download = `${filenameBase()}.svg`;
      a.href = url;
      a.click();
      URL.revokeObjectURL(url);
    };

    // 复制 DataURL(PNG)
    const copyDataURL = async () => {
      const data = canvas.toDataURL('image/png');
      try {
        await navigator.clipboard.writeText(data);
        alert('已复制 PNG DataURL 到剪贴板');
      } catch {
        // 回退:创建临时输入框
        const t = document.createElement('textarea');
        t.value = data; document.body.appendChild(t); t.select();
        document.execCommand('copy'); document.body.removeChild(t);
        alert('已复制 PNG DataURL 到剪贴板');
      }
    };

    // 反色
    const invert = () => {
      const oldDark = darkEl.value, oldLight = lightEl.value;
      darkEl.value = oldLight; lightEl.value = oldDark; generate();
    };

    // 事件绑定(带轻微防抖,输入体验更好)
    textEl.addEventListener('input', debounce(generate, 180));
    
    // 所有控制元素的变化事件
    [levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El, 
     styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl, 
     logoSizeEl, artEffectEl, animationEl].forEach(el => 
      el.addEventListener('input', generate)
    );
    
    // 按钮事件
    $('#qr-download-png').addEventListener('click', downloadPNG);
    $('#qr-download-svg').addEventListener('click', downloadSVG);
    $('#qr-copy-dataurl').addEventListener('click', copyDataURL);
    $('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
    invertEl.addEventListener('click', invert);
    $('#qr-random-colors').addEventListener('click', randomColors);
    $('#qr-clear-logo').addEventListener('click', clearLogo);
    $('#qr-test-link').addEventListener('click', testLink);
    
    // Logo上传
    logoEl.addEventListener('change', handleLogoUpload);
    
    // 模态框事件
    modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
    $('#qr-open-link').addEventListener('click', openLink);
    $('#qr-copy-link').addEventListener('click', copyLink);
    
    // 点击模态框外部关闭
    window.addEventListener('click', (event) => {
      if (event.target === linkModal) {
        linkModal.style.display = 'none';
      }
    });
    
    // 复选框事件
    autoRedirectEl.addEventListener('change', generate);
    newWindowEl.addEventListener('change', generate);

    // 首次渲染
    generate();
  });
</script><!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
  <style>
    .qr-card {
      max-width: 720px;
      margin: 1.5rem auto;
      padding: 1rem;
      border: 1px solid #e5e7eb;
      border-radius: 16px;
      background: #fff;
      box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
    }

    .qr-row {
      display: flex;
      flex-wrap: wrap;
      gap: .75rem;
      align-items: center;
      margin: .5rem 0
    }

    .qr-row label {
      font-size: .9rem;
      color: #374151
    }

    .qr-row input[type="text"] {
      flex: 1 1 320px;
      padding: .6rem .8rem;
      border: 1px solid #d1d5db;
      border-radius: 10px
    }

    .qr-row input[type="number"] {
      width: 5.5rem;
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px
    }

    .qr-row select,
    .qr-row input[type="color"] {
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px;
      background: #fff
    }

    .qr-actions {
      display: flex;
      gap: .6rem;
      flex-wrap: wrap;
      margin-top: .75rem
    }

    .qr-btn {
      appearance: none;
      border: 1px solid #111827;
      background: #111827;
      color: #fff;
      padding: .55rem .9rem;
      border-radius: 999px;
      cursor: pointer
    }

    .qr-btn.alt {
      border-color: #d1d5db;
      background: #fff;
      color: #111827
    }

    .qr-preview {
      display: grid;
      place-items: center;
      background: #f9fafb;
      border-radius: 12px;
      padding: 1rem;
      margin-top: .75rem
    }

    .qr-size-output {
      min-width: 3ch;
      text-align: right;
      font-variant-numeric: tabular-nums
    }

    .qr-row input[type="checkbox"] {
      width: auto;
      margin: 0 0.5rem;
    }

    .qr-row input[type="file"] {
      font-size: 0.8rem;
      padding: 0.3rem;
    }

    /* 二维码艺术效果样式 */
    .qr-shadow {
      filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
    }

    .qr-glow {
      filter: drop-shadow(0 0 10px currentColor);
    }

    .qr-3d {
      filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
    }

    .qr-neon {
      filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
    }

    /* 动画效果 */
    .qr-pulse {
      animation: qr-pulse 2s ease-in-out infinite;
    }

    .qr-rotate {
      animation: qr-rotate 4s linear infinite;
    }

    .qr-fade {
      animation: qr-fade 3s ease-in-out infinite;
    }

    @keyframes qr-pulse {
      0%, 100% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.05); opacity: 0.8; }
    }

    @keyframes qr-rotate {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    @keyframes qr-fade {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.6; }
    }

    /* 链接测试模态框 */
    .qr-modal {
      display: none;
      position: fixed;
      z-index: 1000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
    }

    .qr-modal-content {
      background-color: #fefefe;
      margin: 15% auto;
      padding: 20px;
      border: none;
      border-radius: 12px;
      width: 300px;
      text-align: center;
    }

    .qr-modal-close {
      color: #aaa;
      float: right;
      font-size: 28px;
      font-weight: bold;
      cursor: pointer;
    }

    @media (prefers-color-scheme: dark) {
      .qr-card {
        background: #111827;
        border-color: #374151;
        color: #e5e7eb
      }

      .qr-row input,
      .qr-row select {
        background: #0b1220;
        color: #e5e7eb;
        border-color: #374151
      }

      .qr-preview {
        background: #0b1220
      }

      .qr-btn.alt {
        background: transparent;
        color: #e5e7eb;
        border-color: #4b5563
      }

      .qr-modal-content {
        background-color: #1f2937;
        color: #e5e7eb;
      }

      .qr-modal-close {
        color: #9ca3af;
      }
    }
  </style>

  <div class="qr-row">
    <label>内容:</label>
    <input id="qr-text" type="text" placeholder="要编码的文本/链接" />
    <button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
  </div>

  <div class="qr-row">
    <label>纠错:</label>
    <select id="qr-level">
      <option value="L">L(约7%)</option>
      <option value="M" selected>M(约15%)</option>
      <option value="Q">Q(约25%)</option>
      <option value="H">H(约30%)</option>
    </select>

    <label>尺寸(px):</label>
    <input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
    <span class="qr-size-output" id="qr-size-out">256</span>

    <label>边距(模块):</label>
    <input id="qr-margin" type="number" min="0" max="8" value="4" />
  </div>

  <div class="qr-row">
    <label>样式模式:</label>
    <select id="qr-style-mode">
      <option value="classic">经典方块</option>
      <option value="rounded">圆角方块</option>
      <option value="dots">圆点</option>
      <option value="artistic">艺术风格</option>
    </select>
    
    <label>圆角程度:</label>
    <input id="qr-border-radius" type="range" min="0" max="50" value="0" />
    <span class="qr-size-output" id="qr-radius-out">0%</span>
  </div>

  <div class="qr-row">
    <label>颜色模式:</label>
    <select id="qr-color-mode">
      <option value="solid">单色</option>
      <option value="gradient">线性渐变</option>
      <option value="radial">径向渐变</option>
      <option value="rainbow">彩虹渐变</option>
    </select>
    
    <label>渐变方向:</label>
    <select id="qr-gradient-direction">
      <option value="0">水平→</option>
      <option value="90">垂直↓</option>
      <option value="45">对角↘</option>
      <option value="135">对角↙</option>
    </select>
  </div>

  <div class="qr-row">
    <label>前景色:</label>
    <input id="qr-dark" type="color" value="#000000" />
    <label>渐变终点色:</label>
    <input id="qr-dark2" type="color" value="#4f46e5" />
    <button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
  </div>

  <div class="qr-row">
    <label>背景色:</label>
    <input id="qr-light" type="color" value="#ffffff" />
    <label>背景渐变色:</label>
    <input id="qr-light2" type="color" value="#f3f4f6" />
    <button class="qr-btn alt" id="qr-invert" type="button">反色</button>
  </div>

  <div class="qr-row">
    <label>Logo图片:</label>
    <input id="qr-logo" type="file" accept="image/*" />
    <label>Logo大小:</label>
    <input id="qr-logo-size" type="range" min="10" max="30" value="15" />
    <span class="qr-size-output" id="qr-logo-size-out">15%</span>
    <button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
  </div>

  <div class="qr-row">
    <label>艺术效果:</label>
    <select id="qr-art-effect">
      <option value="none">无</option>
      <option value="shadow">阴影</option>
      <option value="glow">发光</option>
      <option value="3d">3D效果</option>
      <option value="neon">霓虹</option>
    </select>
    
    <label>动画效果:</label>
    <select id="qr-animation">
      <option value="none">无动画</option>
      <option value="pulse">脉冲</option>
      <option value="rotate">旋转</option>
      <option value="fade">淡入淡出</option>
    </select>
  </div>

  <div class="qr-row">
    <label>链接自动跳转:</label>
    <input id="qr-auto-redirect" type="checkbox" />
    <label>新窗口打开:</label>
    <input id="qr-new-window" type="checkbox" checked />
    <button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
  </div>

  <div class="qr-actions">
    <button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
    <button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
    <button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
  </div>

  <div class="qr-preview">
    <div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
  </div>
  <canvas id="qr-canvas" style="display:none"></canvas>

  <!-- 链接测试模态框 -->
  <div id="qr-link-modal" class="qr-modal">
    <div class="qr-modal-content">
      <span class="qr-modal-close">&times;</span>
      <h3>链接测试</h3>
      <p id="qr-link-preview"></p>
      <div style="margin-top: 15px;">
        <button class="qr-btn" id="qr-open-link">打开链接</button>
        <button class="qr-btn alt" id="qr-copy-link">复制链接</button>
      </div>
    </div>
  </div>
</div>

<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>

<script>
  // 等待库加载完成
  window.addEventListener('load', () => {
    const $ = (sel) => document.querySelector(sel);
    const textEl = $('#qr-text');
    const levelEl = $('#qr-level');
    const sizeEl = $('#qr-size');
    const sizeOut = $('#qr-size-out');
    const marginEl = $('#qr-margin');
    const darkEl = $('#qr-dark');
    const dark2El = $('#qr-dark2');
    const lightEl = $('#qr-light');
    const light2El = $('#qr-light2');
    const invertEl = $('#qr-invert');
    const svgWrap = $('#qr-svg-wrap');
    const canvas = $('#qr-canvas');
    
    // 新增的控制元素
    const styleModeEl = $('#qr-style-mode');
    const borderRadiusEl = $('#qr-border-radius');
    const radiusOut = $('#qr-radius-out');
    const colorModeEl = $('#qr-color-mode');
    const gradientDirectionEl = $('#qr-gradient-direction');
    const logoEl = $('#qr-logo');
    const logoSizeEl = $('#qr-logo-size');
    const logoSizeOut = $('#qr-logo-size-out');
    const artEffectEl = $('#qr-art-effect');
    const animationEl = $('#qr-animation');
    const autoRedirectEl = $('#qr-auto-redirect');
    const newWindowEl = $('#qr-new-window');
    
    // 模态框元素
    const linkModal = $('#qr-link-modal');
    const linkPreview = $('#qr-link-preview');
    const modalClose = $('.qr-modal-close');
    
    let logoImage = null;

    // Canvas圆角矩形兼容性
    if (!CanvasRenderingContext2D.prototype.roundRect) {
      CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
        this.beginPath();
        this.moveTo(x + radius, y);
        this.lineTo(x + width - radius, y);
        this.quadraticCurveTo(x + width, y, x + width, y + radius);
        this.lineTo(x + width, y + height - radius);
        this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        this.lineTo(x + radius, y + height);
        this.quadraticCurveTo(x, y + height, x, y + height - radius);
        this.lineTo(x, y + radius);
        this.quadraticCurveTo(x, y, x + radius, y);
        this.closePath();
      };
    }

    // 初始值:默认填入当前页 URL
    if (!textEl.value) textEl.value = location.href;

    const debounce = (fn, wait = 160) => {
      let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
    };

    function makeQRMatrix(content, level) {
      const qr = qrcode(0, level); // 0 = 自动尺寸
      qr.addData(content);
      qr.make();
      return qr;
    }

    // 颜色工具函数
    function getRandomColor() {
      return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
    }

    function createGradient(color1, color2, direction, type = 'linear') {
      if (type === 'radial') {
        return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
      } else if (type === 'rainbow') {
        return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
      }
      return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
    }

    function renderSVG(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid',
        gradientDirection = 0,
        artEffect = 'none',
        animation = 'none'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const radius = borderRadius / 100 * 0.4; // 转换为相对单位
      
      let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
      
      // 添加渐变定义
      if (colorMode !== 'solid') {
        svg += '<defs>';
        
        // 前景渐变
        if (colorMode === 'gradient') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</linearGradient>';
        } else if (colorMode === 'radial') {
          svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</radialGradient>';
        } else if (colorMode === 'rainbow') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
          const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
          colors.forEach((color, i) => {
            svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
          });
          svg += '</linearGradient>';
        }
        
        // 背景渐变
        svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
        svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
        svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
        svg += '</linearGradient>';
        
        svg += '</defs>';
      }
      
      // 背景
      const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
      svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
      
      // 前景色
      const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = c + border;
            const y = r + border;
            
            if (styleMode === 'dots') {
              svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
            } else if (styleMode === 'rounded' || radius > 0) {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
            } else if (styleMode === 'artistic') {
              // 艺术风格:随机大小和透明度
              const size = 0.8 + Math.random() * 0.4;
              const opacity = 0.7 + Math.random() * 0.3;
              const offset = (1 - size) / 2;
              svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
            } else {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
            }
          }
        }
      }
      
      svg += `</svg>`;
      return svg;
    }

    function renderCanvas(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const scale = Math.max(1, Math.floor(size / units));
      canvas.width = units * scale;
      canvas.height = units * scale;
      const ctx = canvas.getContext('2d');
      
      // 背景渐变
      if (colorMode !== 'solid') {
        const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        bgGradient.addColorStop(0, light);
        bgGradient.addColorStop(1, light2);
        ctx.fillStyle = bgGradient;
      } else {
        ctx.fillStyle = light;
      }
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      
      // 前景渐变
      let fgStyle = dark;
      if (colorMode === 'gradient') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'radial') {
        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;
        const radius = Math.min(centerX, centerY);
        const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'rainbow') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
        colors.forEach((color, i) => {
          fgGradient.addColorStop(i / (colors.length - 1), color);
        });
        fgStyle = fgGradient;
      }
      
      ctx.fillStyle = fgStyle;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = (c + border) * scale;
            const y = (r + border) * scale;
            
            if (styleMode === 'dots') {
              ctx.beginPath();
              ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
              ctx.fill();
            } else if (styleMode === 'rounded' || borderRadius > 0) {
              const radius = (borderRadius / 100) * scale * 0.4;
              ctx.beginPath();
              ctx.roundRect(x, y, scale, scale, radius);
              ctx.fill();
            } else {
              ctx.fillRect(x, y, scale, scale);
            }
          }
        }
      }
      
      // 添加Logo
      if (logoImage) {
        const logoSize = (logoSizeEl.value / 100) * size;
        const logoX = (canvas.width - logoSize) / 2;
        const logoY = (canvas.height - logoSize) / 2;
        
        // 白色背景圆形
        ctx.fillStyle = 'white';
        ctx.beginPath();
        ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
        ctx.fill();
        
        // 绘制Logo
        ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
      }
    }

    // 辅助函数
    function isValidUrl(string) {
      try {
        new URL(string);
        return true;
      } catch (_) {
        return false;  
      }
    }

    function filenameBase() {
      const raw = (textEl.value || '').trim();
      try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
    }

    // Logo处理
    function handleLogoUpload(event) {
      const file = event.target.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = function(e) {
          const img = new Image();
          img.onload = function() {
            logoImage = img;
            generate();
          };
          img.src = e.target.result;
        };
        reader.readAsDataURL(file);
      }
    }

    // 随机配色
    function randomColors() {
      darkEl.value = getRandomColor();
      dark2El.value = getRandomColor();
      lightEl.value = getRandomColor();
      light2El.value = getRandomColor();
      generate();
    }

    // 清除Logo
    function clearLogo() {
      logoImage = null;
      logoEl.value = '';
      generate();
    }

    // 链接测试
    function testLink() {
      const content = textEl.value.trim();
      if (!content) {
        alert('请先输入内容');
        return;
      }
      
      linkPreview.textContent = content;
      linkModal.style.display = 'block';
    }

    // 打开链接
    function openLink() {
      const content = textEl.value.trim();
      if (isValidUrl(content)) {
        if (newWindowEl.checked) {
          window.open(content, '_blank');
        } else {
          window.location.href = content;
        }
      } else {
        alert('不是有效的URL链接');
      }
      linkModal.style.display = 'none';
    }

    // 复制链接
    async function copyLink() {
      const content = textEl.value.trim();
      try {
        await navigator.clipboard.writeText(content);
        alert('链接已复制到剪贴板');
      } catch {
        const t = document.createElement('textarea');
        t.value = content;
        document.body.appendChild(t);
        t.select();
        document.execCommand('copy');
        document.body.removeChild(t);
        alert('链接已复制到剪贴板');
      }
      linkModal.style.display = 'none';
    }

    const generate = () => {
      const content = (textEl.value || '').trim();
      if (!content) { 
        svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>'; 
        return; 
      }
      
      const level = levelEl.value || 'M';
      const size = parseInt(sizeEl.value, 10) || 256;
      const margin = parseInt(marginEl.value, 10) || 4;
      
      const options = {
        dark: darkEl.value || '#000000',
        dark2: dark2El.value || '#4f46e5',
        light: lightEl.value || '#ffffff',
        light2: light2El.value || '#f3f4f6',
        styleMode: styleModeEl.value || 'classic',
        borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
        colorMode: colorModeEl.value || 'solid',
        gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
        artEffect: artEffectEl.value || 'none',
        animation: animationEl.value || 'none'
      };

      const qr = makeQRMatrix(content, level);
      const svg = renderSVG(qr, size, margin, options);
      
      // 应用艺术效果和动画
      let finalSvg = svg;
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = svg;
      const svgElement = tempDiv.querySelector('svg');
      
      if (options.artEffect !== 'none') {
        svgElement.classList.add(`qr-${options.artEffect}`);
      }
      
      if (options.animation !== 'none') {
        svgElement.classList.add(`qr-${options.animation}`);
      }
      
      svgWrap.innerHTML = tempDiv.innerHTML;
      renderCanvas(qr, size, margin, options);
      
      sizeOut.textContent = size;
      radiusOut.textContent = options.borderRadius + '%';
      logoSizeOut.textContent = logoSizeEl.value + '%';
      
      // 自动跳转功能
      if (autoRedirectEl.checked && isValidUrl(content)) {
        svgWrap.style.cursor = 'pointer';
        svgWrap.title = '点击访问链接';
        svgWrap.onclick = () => {
          if (newWindowEl.checked) {
            window.open(content, '_blank');
          } else {
            window.location.href = content;
          }
        };
      } else {
        svgWrap.style.cursor = 'default';
        svgWrap.title = '';
        svgWrap.onclick = null;
      }
    };

    // 下载 PNG
    const downloadPNG = () => {
      const a = document.createElement('a');
      a.download = `${filenameBase()}.png`;
      a.href = canvas.toDataURL('image/png');
      a.click();
    };

    // 下载 SVG
    const downloadSVG = () => {
      const svgEl = svgWrap.querySelector('svg');
      if (!svgEl) return;
      const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.download = `${filenameBase()}.svg`;
      a.href = url;
      a.click();
      URL.revokeObjectURL(url);
    };

    // 复制 DataURL(PNG)
    const copyDataURL = async () => {
      const data = canvas.toDataURL('image/png');
      try {
        await navigator.clipboard.writeText(data);
        alert('已复制 PNG DataURL 到剪贴板');
      } catch {
        // 回退:创建临时输入框
        const t = document.createElement('textarea');
        t.value = data; document.body.appendChild(t); t.select();
        document.execCommand('copy'); document.body.removeChild(t);
        alert('已复制 PNG DataURL 到剪贴板');
      }
    };

    // 反色
    const invert = () => {
      const oldDark = darkEl.value, oldLight = lightEl.value;
      darkEl.value = oldLight; lightEl.value = oldDark; generate();
    };

    // 事件绑定(带轻微防抖,输入体验更好)
    textEl.addEventListener('input', debounce(generate, 180));
    
    // 所有控制元素的变化事件
    [levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El, 
     styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl, 
     logoSizeEl, artEffectEl, animationEl].forEach(el => 
      el.addEventListener('input', generate)
    );
    
    // 按钮事件
    $('#qr-download-png').addEventListener('click', downloadPNG);
    $('#qr-download-svg').addEventListener('click', downloadSVG);
    $('#qr-copy-dataurl').addEventListener('click', copyDataURL);
    $('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
    invertEl.addEventListener('click', invert);
    $('#qr-random-colors').addEventListener('click', randomColors);
    $('#qr-clear-logo').addEventListener('click', clearLogo);
    $('#qr-test-link').addEventListener('click', testLink);
    
    // Logo上传
    logoEl.addEventListener('change', handleLogoUpload);
    
    // 模态框事件
    modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
    $('#qr-open-link').addEventListener('click', openLink);
    $('#qr-copy-link').addEventListener('click', copyLink);
    
    // 点击模态框外部关闭
    window.addEventListener('click', (event) => {
      if (event.target === linkModal) {
        linkModal.style.display = 'none';
      }
    });
    
    // 复选框事件
    autoRedirectEl.addEventListener('change', generate);
    newWindowEl.addEventListener('change', generate);

    // 首次渲染
    generate();
  });
</script><!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
  <style>
    .qr-card {
      max-width: 720px;
      margin: 1.5rem auto;
      padding: 1rem;
      border: 1px solid #e5e7eb;
      border-radius: 16px;
      background: #fff;
      box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
    }

    .qr-row {
      display: flex;
      flex-wrap: wrap;
      gap: .75rem;
      align-items: center;
      margin: .5rem 0
    }

    .qr-row label {
      font-size: .9rem;
      color: #374151
    }

    .qr-row input[type="text"] {
      flex: 1 1 320px;
      padding: .6rem .8rem;
      border: 1px solid #d1d5db;
      border-radius: 10px
    }

    .qr-row input[type="number"] {
      width: 5.5rem;
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px
    }

    .qr-row select,
    .qr-row input[type="color"] {
      padding: .4rem .5rem;
      border: 1px solid #d1d5db;
      border-radius: 8px;
      background: #fff
    }

    .qr-actions {
      display: flex;
      gap: .6rem;
      flex-wrap: wrap;
      margin-top: .75rem
    }

    .qr-btn {
      appearance: none;
      border: 1px solid #111827;
      background: #111827;
      color: #fff;
      padding: .55rem .9rem;
      border-radius: 999px;
      cursor: pointer
    }

    .qr-btn.alt {
      border-color: #d1d5db;
      background: #fff;
      color: #111827
    }

    .qr-preview {
      display: grid;
      place-items: center;
      background: #f9fafb;
      border-radius: 12px;
      padding: 1rem;
      margin-top: .75rem
    }

    .qr-size-output {
      min-width: 3ch;
      text-align: right;
      font-variant-numeric: tabular-nums
    }

    .qr-row input[type="checkbox"] {
      width: auto;
      margin: 0 0.5rem;
    }

    .qr-row input[type="file"] {
      font-size: 0.8rem;
      padding: 0.3rem;
    }

    /* 二维码艺术效果样式 */
    .qr-shadow {
      filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
    }

    .qr-glow {
      filter: drop-shadow(0 0 10px currentColor);
    }

    .qr-3d {
      filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
    }

    .qr-neon {
      filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
    }

    /* 动画效果 */
    .qr-pulse {
      animation: qr-pulse 2s ease-in-out infinite;
    }

    .qr-rotate {
      animation: qr-rotate 4s linear infinite;
    }

    .qr-fade {
      animation: qr-fade 3s ease-in-out infinite;
    }

    @keyframes qr-pulse {
      0%, 100% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.05); opacity: 0.8; }
    }

    @keyframes qr-rotate {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    @keyframes qr-fade {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.6; }
    }

    /* 链接测试模态框 */
    .qr-modal {
      display: none;
      position: fixed;
      z-index: 1000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
    }

    .qr-modal-content {
      background-color: #fefefe;
      margin: 15% auto;
      padding: 20px;
      border: none;
      border-radius: 12px;
      width: 300px;
      text-align: center;
    }

    .qr-modal-close {
      color: #aaa;
      float: right;
      font-size: 28px;
      font-weight: bold;
      cursor: pointer;
    }

    @media (prefers-color-scheme: dark) {
      .qr-card {
        background: #111827;
        border-color: #374151;
        color: #e5e7eb
      }

      .qr-row input,
      .qr-row select {
        background: #0b1220;
        color: #e5e7eb;
        border-color: #374151
      }

      .qr-preview {
        background: #0b1220
      }

      .qr-btn.alt {
        background: transparent;
        color: #e5e7eb;
        border-color: #4b5563
      }

      .qr-modal-content {
        background-color: #1f2937;
        color: #e5e7eb;
      }

      .qr-modal-close {
        color: #9ca3af;
      }
    }
  </style>

  <div class="qr-row">
    <label>内容:</label>
    <input id="qr-text" type="text" placeholder="要编码的文本/链接" />
    <button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
  </div>

  <div class="qr-row">
    <label>纠错:</label>
    <select id="qr-level">
      <option value="L">L(约7%)</option>
      <option value="M" selected>M(约15%)</option>
      <option value="Q">Q(约25%)</option>
      <option value="H">H(约30%)</option>
    </select>

    <label>尺寸(px):</label>
    <input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
    <span class="qr-size-output" id="qr-size-out">256</span>

    <label>边距(模块):</label>
    <input id="qr-margin" type="number" min="0" max="8" value="4" />
  </div>

  <div class="qr-row">
    <label>样式模式:</label>
    <select id="qr-style-mode">
      <option value="classic">经典方块</option>
      <option value="rounded">圆角方块</option>
      <option value="dots">圆点</option>
      <option value="artistic">艺术风格</option>
    </select>
    
    <label>圆角程度:</label>
    <input id="qr-border-radius" type="range" min="0" max="50" value="0" />
    <span class="qr-size-output" id="qr-radius-out">0%</span>
  </div>

  <div class="qr-row">
    <label>颜色模式:</label>
    <select id="qr-color-mode">
      <option value="solid">单色</option>
      <option value="gradient">线性渐变</option>
      <option value="radial">径向渐变</option>
      <option value="rainbow">彩虹渐变</option>
    </select>
    
    <label>渐变方向:</label>
    <select id="qr-gradient-direction">
      <option value="0">水平→</option>
      <option value="90">垂直↓</option>
      <option value="45">对角↘</option>
      <option value="135">对角↙</option>
    </select>
  </div>

  <div class="qr-row">
    <label>前景色:</label>
    <input id="qr-dark" type="color" value="#000000" />
    <label>渐变终点色:</label>
    <input id="qr-dark2" type="color" value="#4f46e5" />
    <button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
  </div>

  <div class="qr-row">
    <label>背景色:</label>
    <input id="qr-light" type="color" value="#ffffff" />
    <label>背景渐变色:</label>
    <input id="qr-light2" type="color" value="#f3f4f6" />
    <button class="qr-btn alt" id="qr-invert" type="button">反色</button>
  </div>

  <div class="qr-row">
    <label>Logo图片:</label>
    <input id="qr-logo" type="file" accept="image/*" />
    <label>Logo大小:</label>
    <input id="qr-logo-size" type="range" min="10" max="30" value="15" />
    <span class="qr-size-output" id="qr-logo-size-out">15%</span>
    <button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
  </div>

  <div class="qr-row">
    <label>艺术效果:</label>
    <select id="qr-art-effect">
      <option value="none">无</option>
      <option value="shadow">阴影</option>
      <option value="glow">发光</option>
      <option value="3d">3D效果</option>
      <option value="neon">霓虹</option>
    </select>
    
    <label>动画效果:</label>
    <select id="qr-animation">
      <option value="none">无动画</option>
      <option value="pulse">脉冲</option>
      <option value="rotate">旋转</option>
      <option value="fade">淡入淡出</option>
    </select>
  </div>

  <div class="qr-row">
    <label>链接自动跳转:</label>
    <input id="qr-auto-redirect" type="checkbox" />
    <label>新窗口打开:</label>
    <input id="qr-new-window" type="checkbox" checked />
    <button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
  </div>

  <div class="qr-actions">
    <button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
    <button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
    <button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
  </div>

  <div class="qr-preview">
    <div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
  </div>
  <canvas id="qr-canvas" style="display:none"></canvas>

  <!-- 链接测试模态框 -->
  <div id="qr-link-modal" class="qr-modal">
    <div class="qr-modal-content">
      <span class="qr-modal-close">&times;</span>
      <h3>链接测试</h3>
      <p id="qr-link-preview"></p>
      <div style="margin-top: 15px;">
        <button class="qr-btn" id="qr-open-link">打开链接</button>
        <button class="qr-btn alt" id="qr-copy-link">复制链接</button>
      </div>
    </div>
  </div>
</div>

<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>

<script>
  // 等待库加载完成
  window.addEventListener('load', () => {
    const $ = (sel) => document.querySelector(sel);
    const textEl = $('#qr-text');
    const levelEl = $('#qr-level');
    const sizeEl = $('#qr-size');
    const sizeOut = $('#qr-size-out');
    const marginEl = $('#qr-margin');
    const darkEl = $('#qr-dark');
    const dark2El = $('#qr-dark2');
    const lightEl = $('#qr-light');
    const light2El = $('#qr-light2');
    const invertEl = $('#qr-invert');
    const svgWrap = $('#qr-svg-wrap');
    const canvas = $('#qr-canvas');
    
    // 新增的控制元素
    const styleModeEl = $('#qr-style-mode');
    const borderRadiusEl = $('#qr-border-radius');
    const radiusOut = $('#qr-radius-out');
    const colorModeEl = $('#qr-color-mode');
    const gradientDirectionEl = $('#qr-gradient-direction');
    const logoEl = $('#qr-logo');
    const logoSizeEl = $('#qr-logo-size');
    const logoSizeOut = $('#qr-logo-size-out');
    const artEffectEl = $('#qr-art-effect');
    const animationEl = $('#qr-animation');
    const autoRedirectEl = $('#qr-auto-redirect');
    const newWindowEl = $('#qr-new-window');
    
    // 模态框元素
    const linkModal = $('#qr-link-modal');
    const linkPreview = $('#qr-link-preview');
    const modalClose = $('.qr-modal-close');
    
    let logoImage = null;

    // Canvas圆角矩形兼容性
    if (!CanvasRenderingContext2D.prototype.roundRect) {
      CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
        this.beginPath();
        this.moveTo(x + radius, y);
        this.lineTo(x + width - radius, y);
        this.quadraticCurveTo(x + width, y, x + width, y + radius);
        this.lineTo(x + width, y + height - radius);
        this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        this.lineTo(x + radius, y + height);
        this.quadraticCurveTo(x, y + height, x, y + height - radius);
        this.lineTo(x, y + radius);
        this.quadraticCurveTo(x, y, x + radius, y);
        this.closePath();
      };
    }

    // 初始值:默认填入当前页 URL
    if (!textEl.value) textEl.value = location.href;

    const debounce = (fn, wait = 160) => {
      let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
    };

    function makeQRMatrix(content, level) {
      const qr = qrcode(0, level); // 0 = 自动尺寸
      qr.addData(content);
      qr.make();
      return qr;
    }

    // 颜色工具函数
    function getRandomColor() {
      return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
    }

    function createGradient(color1, color2, direction, type = 'linear') {
      if (type === 'radial') {
        return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
      } else if (type === 'rainbow') {
        return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
      }
      return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
    }

    function renderSVG(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid',
        gradientDirection = 0,
        artEffect = 'none',
        animation = 'none'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const radius = borderRadius / 100 * 0.4; // 转换为相对单位
      
      let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
      
      // 添加渐变定义
      if (colorMode !== 'solid') {
        svg += '<defs>';
        
        // 前景渐变
        if (colorMode === 'gradient') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</linearGradient>';
        } else if (colorMode === 'radial') {
          svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
          svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
          svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
          svg += '</radialGradient>';
        } else if (colorMode === 'rainbow') {
          svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
          const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
          colors.forEach((color, i) => {
            svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
          });
          svg += '</linearGradient>';
        }
        
        // 背景渐变
        svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
        svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
        svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
        svg += '</linearGradient>';
        
        svg += '</defs>';
      }
      
      // 背景
      const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
      svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
      
      // 前景色
      const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = c + border;
            const y = r + border;
            
            if (styleMode === 'dots') {
              svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
            } else if (styleMode === 'rounded' || radius > 0) {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
            } else if (styleMode === 'artistic') {
              // 艺术风格:随机大小和透明度
              const size = 0.8 + Math.random() * 0.4;
              const opacity = 0.7 + Math.random() * 0.3;
              const offset = (1 - size) / 2;
              svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
            } else {
              svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
            }
          }
        }
      }
      
      svg += `</svg>`;
      return svg;
    }

    function renderCanvas(qr, size, margin, options = {}) {
      const {
        dark = '#000000',
        dark2 = '#4f46e5',
        light = '#ffffff',
        light2 = '#f3f4f6',
        styleMode = 'classic',
        borderRadius = 0,
        colorMode = 'solid'
      } = options;

      const n = qr.getModuleCount();
      const border = Math.max(0, margin | 0);
      const units = n + 2 * border;
      const scale = Math.max(1, Math.floor(size / units));
      canvas.width = units * scale;
      canvas.height = units * scale;
      const ctx = canvas.getContext('2d');
      
      // 背景渐变
      if (colorMode !== 'solid') {
        const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        bgGradient.addColorStop(0, light);
        bgGradient.addColorStop(1, light2);
        ctx.fillStyle = bgGradient;
      } else {
        ctx.fillStyle = light;
      }
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      
      // 前景渐变
      let fgStyle = dark;
      if (colorMode === 'gradient') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'radial') {
        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;
        const radius = Math.min(centerX, centerY);
        const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
        fgGradient.addColorStop(0, dark);
        fgGradient.addColorStop(1, dark2);
        fgStyle = fgGradient;
      } else if (colorMode === 'rainbow') {
        const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
        colors.forEach((color, i) => {
          fgGradient.addColorStop(i / (colors.length - 1), color);
        });
        fgStyle = fgGradient;
      }
      
      ctx.fillStyle = fgStyle;
      
      // 渲染模块
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (qr.isDark(r, c)) {
            const x = (c + border) * scale;
            const y = (r + border) * scale;
            
            if (styleMode === 'dots') {
              ctx.beginPath();
              ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
              ctx.fill();
            } else if (styleMode === 'rounded' || borderRadius > 0) {
              const radius = (borderRadius / 100) * scale * 0.4;
              ctx.beginPath();
              ctx.roundRect(x, y, scale, scale, radius);
              ctx.fill();
            } else {
              ctx.fillRect(x, y, scale, scale);
            }
          }
        }
      }
      
      // 添加Logo
      if (logoImage) {
        const logoSize = (logoSizeEl.value / 100) * size;
        const logoX = (canvas.width - logoSize) / 2;
        const logoY = (canvas.height - logoSize) / 2;
        
        // 白色背景圆形
        ctx.fillStyle = 'white';
        ctx.beginPath();
        ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
        ctx.fill();
        
        // 绘制Logo
        ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
      }
    }

    // 辅助函数
    function isValidUrl(string) {
      try {
        new URL(string);
        return true;
      } catch (_) {
        return false;  
      }
    }

    function filenameBase() {
      const raw = (textEl.value || '').trim();
      try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
    }

    // Logo处理
    function handleLogoUpload(event) {
      const file = event.target.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = function(e) {
          const img = new Image();
          img.onload = function() {
            logoImage = img;
            generate();
          };
          img.src = e.target.result;
        };
        reader.readAsDataURL(file);
      }
    }

    // 随机配色
    function randomColors() {
      darkEl.value = getRandomColor();
      dark2El.value = getRandomColor();
      lightEl.value = getRandomColor();
      light2El.value = getRandomColor();
      generate();
    }

    // 清除Logo
    function clearLogo() {
      logoImage = null;
      logoEl.value = '';
      generate();
    }

    // 链接测试
    function testLink() {
      const content = textEl.value.trim();
      if (!content) {
        alert('请先输入内容');
        return;
      }
      
      linkPreview.textContent = content;
      linkModal.style.display = 'block';
    }

    // 打开链接
    function openLink() {
      const content = textEl.value.trim();
      if (isValidUrl(content)) {
        if (newWindowEl.checked) {
          window.open(content, '_blank');
        } else {
          window.location.href = content;
        }
      } else {
        alert('不是有效的URL链接');
      }
      linkModal.style.display = 'none';
    }

    // 复制链接
    async function copyLink() {
      const content = textEl.value.trim();
      try {
        await navigator.clipboard.writeText(content);
        alert('链接已复制到剪贴板');
      } catch {
        const t = document.createElement('textarea');
        t.value = content;
        document.body.appendChild(t);
        t.select();
        document.execCommand('copy');
        document.body.removeChild(t);
        alert('链接已复制到剪贴板');
      }
      linkModal.style.display = 'none';
    }

    const generate = () => {
      const content = (textEl.value || '').trim();
      if (!content) { 
        svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>'; 
        return; 
      }
      
      const level = levelEl.value || 'M';
      const size = parseInt(sizeEl.value, 10) || 256;
      const margin = parseInt(marginEl.value, 10) || 4;
      
      const options = {
        dark: darkEl.value || '#000000',
        dark2: dark2El.value || '#4f46e5',
        light: lightEl.value || '#ffffff',
        light2: light2El.value || '#f3f4f6',
        styleMode: styleModeEl.value || 'classic',
        borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
        colorMode: colorModeEl.value || 'solid',
        gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
        artEffect: artEffectEl.value || 'none',
        animation: animationEl.value || 'none'
      };

      const qr = makeQRMatrix(content, level);
      const svg = renderSVG(qr, size, margin, options);
      
      // 应用艺术效果和动画
      let finalSvg = svg;
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = svg;
      const svgElement = tempDiv.querySelector('svg');
      
      if (options.artEffect !== 'none') {
        svgElement.classList.add(`qr-${options.artEffect}`);
      }
      
      if (options.animation !== 'none') {
        svgElement.classList.add(`qr-${options.animation}`);
      }
      
      svgWrap.innerHTML = tempDiv.innerHTML;
      renderCanvas(qr, size, margin, options);
      
      sizeOut.textContent = size;
      radiusOut.textContent = options.borderRadius + '%';
      logoSizeOut.textContent = logoSizeEl.value + '%';
      
      // 自动跳转功能
      if (autoRedirectEl.checked && isValidUrl(content)) {
        svgWrap.style.cursor = 'pointer';
        svgWrap.title = '点击访问链接';
        svgWrap.onclick = () => {
          if (newWindowEl.checked) {
            window.open(content, '_blank');
          } else {
            window.location.href = content;
          }
        };
      } else {
        svgWrap.style.cursor = 'default';
        svgWrap.title = '';
        svgWrap.onclick = null;
      }
    };

    // 下载 PNG
    const downloadPNG = () => {
      const a = document.createElement('a');
      a.download = `${filenameBase()}.png`;
      a.href = canvas.toDataURL('image/png');
      a.click();
    };

    // 下载 SVG
    const downloadSVG = () => {
      const svgEl = svgWrap.querySelector('svg');
      if (!svgEl) return;
      const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.download = `${filenameBase()}.svg`;
      a.href = url;
      a.click();
      URL.revokeObjectURL(url);
    };

    // 复制 DataURL(PNG)
    const copyDataURL = async () => {
      const data = canvas.toDataURL('image/png');
      try {
        await navigator.clipboard.writeText(data);
        alert('已复制 PNG DataURL 到剪贴板');
      } catch {
        // 回退:创建临时输入框
        const t = document.createElement('textarea');
        t.value = data; document.body.appendChild(t); t.select();
        document.execCommand('copy'); document.body.removeChild(t);
        alert('已复制 PNG DataURL 到剪贴板');
      }
    };

    // 反色
    const invert = () => {
      const oldDark = darkEl.value, oldLight = lightEl.value;
      darkEl.value = oldLight; lightEl.value = oldDark; generate();
    };

    // 事件绑定(带轻微防抖,输入体验更好)
    textEl.addEventListener('input', debounce(generate, 180));
    
    // 所有控制元素的变化事件
    [levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El, 
     styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl, 
     logoSizeEl, artEffectEl, animationEl].forEach(el => 
      el.addEventListener('input', generate)
    );
    
    // 按钮事件
    $('#qr-download-png').addEventListener('click', downloadPNG);
    $('#qr-download-svg').addEventListener('click', downloadSVG);
    $('#qr-copy-dataurl').addEventListener('click', copyDataURL);
    $('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
    invertEl.addEventListener('click', invert);
    $('#qr-random-colors').addEventListener('click', randomColors);
    $('#qr-clear-logo').addEventListener('click', clearLogo);
    $('#qr-test-link').addEventListener('click', testLink);
    
    // Logo上传
    logoEl.addEventListener('change', handleLogoUpload);
    
    // 模态框事件
    modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
    $('#qr-open-link').addEventListener('click', openLink);
    $('#qr-copy-link').addEventListener('click', copyLink);
    
    // 点击模态框外部关闭
    window.addEventListener('click', (event) => {
      if (event.target === linkModal) {
        linkModal.style.display = 'none';
      }
    });
    
    // 复选框事件
    autoRedirectEl.addEventListener('change, generate);
    newWindowEl.addEventListener('change', generate);

    // 首次渲染
    generate();
  });
</script>


评论