未命名 7ZfL2Fedit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
index.html
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro 二维码模拟器</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/qr-code-styling.js"></script>
<style>
  :root { --accent: #7b2ff7; --bg-panel: rgba(255,255,255,0.05); --border: rgba(255,255,255,0.08); }
  * { margin:0; padding:0; box-sizing:border-box; }
  body { font-family:'Segoe UI','PingFang SC',sans-serif; background:#0f0c29; color:#e0e0e0; min-height:100vh; }
  
  /* Layout */
  .app { display:flex; flex-wrap:wrap; gap:20px; max-width:1300px; margin:0 auto; padding:20px; }
  .sidebar { flex:1; min-width:340px; max-width:480px; display:flex; flex-direction:column; gap:16px; }
  .main { flex:1; min-width:320px; display:flex; flex-direction:column; align-items:center; gap:20px; }
  
  /* Panels */
  .panel { background:var(--bg-panel); border:1px solid var(--border); border-radius:14px; padding:20px; backdrop-filter:blur(10px); }
  .panel-head { font-size:1rem; font-weight:600; color:#c0c0ff; margin-bottom:14px; display:flex; align-items:center; gap:8px; }
  
  /* Form Elements */
  label { display:block; font-size:.82rem; color:#9999bb; margin-bottom:5px; }
  textarea, input[type=text], input[type=url], select { 
    width:100%; padding:9px 12px; background:rgba(255,255,255,.07); 
    border:1px solid var(--border); border-radius:8px; color:#e0e0e0; 
    font-size:.9rem; outline:none; transition:.3s; 
  }
  textarea:focus, input:focus, select:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(123,47,247,.15); }
  textarea { resize:vertical; min-height:60px; }
  select { cursor:pointer; }
  
  .row { display:flex; gap:10px; }
  .row > * { flex:1; }
  .form-group { margin-bottom:14px; }
  
  /* Color Picker Row */
  .color-row { display:flex; align-items:center; gap:8px; }
  .color-row input[type=color] { width:40px; height:34px; border:none; border-radius:6px; cursor:pointer; padding:0; background:none; }
  .color-row input[type=color]::-webkit-color-swatch-wrapper { padding:2px; }
  .color-row input[type=color]::-webkit-color-swatch { border-radius:4px; border:none; }
  .color-row .hex { font-family:monospace; font-size:.85rem; color:#aaa; min-width:60px; }
  
  /* Range Slider */
  .range-wrap { display:flex; align-items:center; gap:10px; }
  .range-wrap input[type=range] { flex:1; height:5px; -webkit-appearance:none; background:rgba(255,255,255,.1); border-radius:3px; outline:none; }
  .range-wrap input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; width:16px; height:16px; background:var(--accent); border-radius:50%; cursor:pointer; border:2px solid #fff; }
  .range-val { min-width:36px; text-align:center; font-size:.82rem; color:#aaa; background:rgba(255,255,255,.06); padding:3px 6px; border-radius:5px; }
  
  /* Tabs */
  .tabs { display:flex; gap:3px; background:rgba(255,255,255,.04); border-radius:9px; padding:3px; margin-bottom:14px; overflow-x:auto; }
  .tab { flex:1; padding:7px 4px; text-align:center; border:none; background:transparent; color:#8888aa; border-radius:7px; cursor:pointer; font-size:.78rem; white-space:nowrap; transition:.3s; }
  .tab.active { background:rgba(123,47,247,.25); color:#d0d0ff; }
  .tab-pane { display:none; }
  .tab-pane.active { display:block; }
  
  /* Shape Selector */
  .shape-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; }
  .shape-opt { 
    aspect-ratio:1; display:flex; align-items:center; justify-content:center; 
    background:rgba(255,255,255,.04); border:2px solid transparent; border-radius:10px; 
    cursor:pointer; transition:.3s; font-size:1.4rem; 
  }
  .shape-opt:hover { background:rgba(255,255,255,.08); }
  .shape-opt.active { border-color:var(--accent); background:rgba(123,47,247,.1); }
  
  /* Media Upload */
  .media-upload { 
    border:2px dashed var(--border); border-radius:10px; padding:16px; 
    text-align:center; cursor:pointer; transition:.3s; color:#777; font-size:.82rem; 
  }
  .media-upload:hover { border-color:var(--accent); color:#aaa; background:rgba(123,47,247,.04); }
  .media-preview { margin-top:10px; display:flex; align-items:center; gap:10px; background:rgba(255,255,255,.04); padding:8px; border-radius:8px; }
  .media-preview audio, .media-preview video { max-width:100%; height:36px; border-radius:6px; }
  .media-info { flex:1; overflow:hidden; }
  .media-info .name { font-size:.8rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .media-info .size { font-size:.72rem; color:#666; }
  .media-remove { background:none; border:none; color:#666; cursor:pointer; font-size:1rem; padding:4px; }
  .media-remove:hover { color:#f44; }
  
  /* Preview Area */
  .qr-frame { 
    position:relative; background:#fff; padding:20px; border-radius:16px; 
    box-shadow:0 8px 32px rgba(0,0,0,.3); transition:transform .3s, border-radius .4s; 
    display:flex; align-items:center; justify-content:center; min-width:260px; min-height:260px;
  }
  .qr-frame:hover { transform:scale(1.02); }
  #qr-code { display:flex; align-items:center; justify-content:center; }
  
  /* Shape clip-path on the QR wrapper */
  .qr-frame.shape-square { border-radius:16px; }
  .qr-frame.shape-circle { border-radius:50%; }
  .qr-frame.shape-triangle { clip-path:polygon(50% 0%,0% 100%,100% 100%); border-radius:0; padding:30px 20px 10px; }
  .qr-frame.shape-diamond { clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%); border-radius:0; padding:30px; }
  .qr-frame.shape-hexagon { clip-path:polygon(25% 0%,75% 0%,100% 50%,75% 100%,25% 100%,0% 50%); border-radius:0; padding:24px; }
  .qr-frame.shape-star { clip-path:polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); border-radius:0; padding:30px; }
  .qr-frame.shape-heart { clip-path:path('M150 270 C60 200 0 150 0 90 A90 90 0 0 1 150 90 A90 90 0 0 1 300 90 C300 150 240 200 150 270Z'); border-radius:0; padding:20px 20px 30px; }
  .qr-frame.shape-bubble { clip-path:ellipse(50% 45% at 50% 45%); border-radius:0; padding:24px 24px 40px; }
  
  /* Buttons */
  .btn-group { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; }
  .btn { padding:9px 18px; border:none; border-radius:9px; font-size:.85rem; cursor:pointer; font-weight:600; transition:.3s; display:flex; align-items:center; gap:5px; }
  .btn-primary { background:linear-gradient(135deg,#7b2ff7,#5b1fd7); color:#fff; }
  .btn-primary:hover { box-shadow:0 4px 16px rgba(123,47,247,.4); transform:translateY(-1px); }
  .btn-success { background:linear-gradient(135deg,#00c853,#009624); color:#fff; }
  .btn-success:hover { box-shadow:0 4px 16px rgba(0,200,83,.4); transform:translateY(-1px); }
  .btn-outline { background:transparent; color:#aaa; border:1px solid var(--border); }
  .btn-outline:hover { border-color:var(--accent); color:#d0d0ff; }
  
  /* Gradient Preview Bar */
  .grad-bar { width:100%; height:20px; border-radius:5px; margin-top:6px; border:1px solid var(--border); }
  
  /* Checkbox */
  .chk { display:flex; align-items:center; gap:7px; cursor:pointer; font-size:.85rem; color:#bbb; }
  .chk input { width:16px; height:16px; accent-color:var(--accent); cursor:pointer; }
  
  /* Toast */
  .toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:rgba(123,47,247,.92); color:#fff; padding:10px 24px; border-radius:9px; font-size:.85rem; opacity:0; transition:.4s; pointer-events:none; z-index:999; backdrop-filter:blur(8px); }
  .toast.show { opacity:1; }
  
  /* Divider */
  hr { border:none; border-top:1px solid rgba(255,255,255,.06); margin:14px 0; }
  
  /* Responsive */
  @media(max-width:720px) { .app { flex-direction:column; } .sidebar,.main { max-width:100%; } }
  ::-webkit-scrollbar { width:5px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border-radius:3px; }
</style>
</head>
<body>

<div class="app">
  <!-- SIDEBAR -->
  <div class="sidebar">
    
    <!-- Content Panel -->
    <div class="panel">
      <div class="panel-head">📝 内容与跳转</div>
      <div class="form-group">
        <label>二维码内容 / 跳转地址</label>
        <textarea id="qr-data" placeholder="输入网址 https://... 或任意文本">https://github.com</textarea>
      </div>
      <div class="row">
        <div class="form-group">
          <label>容错等级</label>
          <select id="ec-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>
        </div>
        <div class="form-group">
          <label>尺寸 px</label>
          <div class="range-wrap">
            <input type="range" id="qr-size" min="100" max="800" value="300" step="10">
            <span class="range-val" id="size-val">300</span>
          </div>
        </div>
      </div>
      <div class="form-group">
        <label>边距</label>
        <div class="range-wrap">
          <input type="range" id="qr-margin" min="0" max="50" value="10">
          <span class="range-val" id="margin-val">10</span>
        </div>
      </div>
    </div>

    <!-- Style Panel with Tabs -->
    <div class="panel">
      <div class="tabs">
        <button class="tab active" data-t="t-shape">🔷 外形</button>
        <button class="tab" data-t="t-color">🎨 配色</button>
        <button class="tab" data-t="t-dots">⬛ 码点</button>
        <button class="tab" data-t="t-media">🎵 媒体</button>
        <button class="tab" data-t="t-logo">🖼 Logo</button>
      </div>

      <!-- TAB: Shape -->
      <div class="tab-pane active" id="t-shape">
        <label>二维码整体形状</label>
        <div class="shape-grid" id="shape-selector">
          <div class="shape-opt active" data-shape="square" title="正方形">▢</div>
          <div class="shape-opt" data-shape="circle" title="圆形">●</div>
          <div class="shape-opt" data-shape="triangle" title="三角形">▲</div>
          <div class="shape-opt" data-shape="diamond" title="菱形">◆</div>
          <div class="shape-opt" data-shape="hexagon" title="六边形">⬡</div>
          <div class="shape-opt" data-shape="star" title="星形">★</div>
          <div class="shape-opt" data-shape="heart" title="爱心">♥</div>
          <div class="shape-opt" data-shape="bubble" title="气泡">💬</div>
        </div>
        <p style="font-size:.75rem;color:#666;margin-top:10px;">⚠️ 非矩形形状可能影响扫码识别率,建议提高容错等级至 H</p>
      </div>

      <!-- TAB: Color -->
      <div class="tab-pane" id="t-color">
        <div class="form-group">
          <label>前景色</label>
          <div class="color-row">
            <input type="color" id="fg-color" value="#000000">
            <span class="hex" id="fg-hex">#000000</span>
          </div>
        </div>
        <div class="form-group">
          <label>背景色</label>
          <div class="color-row">
            <input type="color" id="bg-color" value="#ffffff">
            <span class="hex" id="bg-hex">#ffffff</span>
          </div>
        </div>
        <hr>
        <label class="chk"><input type="checkbox" id="use-fg-grad"> 前景渐变</label>
        <div id="fg-grad-opts" style="display:none;margin-top:10px;">
          <div class="row">
            <div class="form-group"><label>起始色</label><div class="color-row"><input type="color" id="fg-g1" value="#7b2ff7"></div></div>
            <div class="form-group"><label>结束色</label><div class="color-row"><input type="color" id="fg-g2" value="#00d2ff"></div></div>
          </div>
          <div class="form-group">
            <label>类型</label>
            <select id="fg-grad-type"><option value="linear">线性</option><option value="radial">径向</option></select>
          </div>
          <div class="form-group">
            <label>角度</label>
            <div class="range-wrap"><input type="range" id="fg-grad-angle" min="0" max="360" value="45"><span class="range-val" id="fg-angle-val">45°</span></div>
          </div>
          <div class="grad-bar" id="fg-grad-bar"></div>
        </div>
        <hr>
        <label class="chk"><input type="checkbox" id="use-bg-grad"> 背景渐变</label>
        <div id="bg-grad-opts" style="display:none;margin-top:10px;">
          <div class="row">
            <div class="form-group"><label>起始色</label><div class="color-row"><input type="color" id="bg-g1" value="#f0f0f0"></div></div>
            <div class="form-group"><label>结束色</label><div class="color-row"><input type="color" id="bg-g2" value="#e0e0ff"></div></div>
          </div>
          <div class="form-group">
            <label>类型</label>
            <select id="bg-grad-type"><option value="linear">线性</option><option value="radial">径向</option></select>
          </div>
          <div class="grad-bar" id="bg-grad-bar"></div>
        </div>
        <hr>
        <div class="form-group">
          <label>定位角颜色</label>
          <div class="color-row"><input type="color" id="corner-color" value="#000000"><span class="hex" id="cc-hex">#000000</span></div>
        </div>
        <div class="form-group">
          <label>定位角内点颜色</label>
          <div class="color-row"><input type="color" id="corner-dot-color" value="#000000"><span class="hex" id="cdc-hex">#000000</span></div>
        </div>
      </div>

      <!-- TAB: Dots -->
      <div class="tab-pane" id="t-dots">
        <div class="form-group">
          <label>码点样式</label>
          <select id="dots-style">
            <option value="square">■ 方形</option>
            <option value="dots">● 圆形</option>
            <option value="rounded" selected>▢ 圆角</option>
            <option value="dot">• 小圆点</option>
            <option value="classy">◆ 经典</option>
            <option value="classy-rounded">◇ 经典圆角</option>
            <option value="extra-rounded">⬮ 超圆角</option>
          </select>
        </div>
        <div class="form-group">
          <label>定位角外框</label>
          <select id="cs-style">
            <option value="square" selected>■ 方形</option>
            <option value="dot">● 圆形</option>
            <option value="extra-rounded">⬮ 超圆角</option>
          </select>
        </div>
        <div class="form-group">
          <label>定位角内点</label>
          <select id="cd-style">
            <option value="square" selected>■ 方形</option>
            <option value="dot">● 圆形</option>
          </select>
        </div>
      </div>

      <!-- TAB: Media -->
      <div class="tab-pane" id="t-media">
        <p style="font-size:.8rem;color:#888;margin-bottom:12px;">嵌入音视频文件,扫码后直接在浏览器播放。文件将转为 Base64 内嵌于二维码链接中。<br><b>⚠️ 仅支持小文件 (&lt;2KB),否则二维码过密无法扫描。</b></p>
        <div class="media-upload" id="media-drop">
          🎵🎬 点击或拖拽音频/视频文件<br>
          <span style="font-size:.72rem;color:#555;">支持 mp3/wav/ogg/mp4/webm</span>
          <input type="file" id="media-input" accept="audio/*,video/*" style="display:none;">
        </div>
        <div id="media-preview-area" style="display:none;"></div>
        <div id="media-warn" style="display:none;margin-top:8px;padding:8px;background:rgba(255,80,80,.15);border-radius:6px;font-size:.78rem;color:#f88;"></div>
      </div>

      <!-- TAB: Logo -->
      <div class="tab-pane" id="t-logo">
        <div class="media-upload" id="logo-drop">
          🖼 点击或拖拽 Logo 图片
          <input type="file" id="logo-input" accept="image/*" style="display:none;">
        </div>
        <div id="logo-preview-area" style="display:none;text-align:center;margin-top:10px;">
          <img id="logo-thumb" style="max-width:60px;max-height:60px;border-radius:8px;">
          <br><button class="btn btn-outline" style="margin-top:6px;font-size:.75rem;padding:5px 12px;" onclick="removeLogo()">移除</button>
        </div>
        <hr>
        <div class="form-group">
          <label>Logo 大小</label>
          <div class="range-wrap"><input type="range" id="logo-size" min="0.1" max="0.5" step="0.05" value="0.3"><span class="range-val" id="ls-val">0.3</span></div>
        </div>
        <div class="form-group">
          <label>Logo 边距</label>
          <div class="range-wrap"><input type="range" id="logo-margin" min="0" max="20" value="5"><span class="range-val" id="lm-val">5</span></div>
        </div>
        <label class="chk"><input type="checkbox" id="logo-hide-bg" checked> 隐藏Logo下方码点</label>
      </div>
    </div>
  </div>

  <!-- MAIN PREVIEW -->
  <div class="main">
    <div class="panel" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:18px;">
      <div class="panel-head">👁️ 实时预览</div>
      <div class="qr-frame shape-square" id="qr-frame">
        <div id="qr-code"></div>
      </div>
      <div class="btn-group">
        <button class="btn btn-primary" onclick="generateQR()">🔄 生成</button>
        <button class="btn btn-success" onclick="dl('png')">📥 PNG</button>
        <button class="btn btn-success" onclick="dl('jpeg')">📥 JPEG</button>
        <button class="btn btn-success" onclick="dl('svg')">📥 SVG</button>
      </div>
    </div>
    
    <div class="panel" style="width:100%;">
      <div class="panel-head">💡 使用说明</div>
      <ul style="font-size:.82rem;color:#999;padding-left:18px;line-height:1.8;">
        <li><b>跳转网站:</b>在内容框输入完整 URL(含 https://)</li>
        <li><b>播放媒体:</b>上传小体积音视频,自动生成 data URI 嵌入二维码</li>
        <li><b>自定义形状:</b>通过 CSS clip-path 裁剪预览容器,导出图片保持原始方形以确保可扫</li>
        <li><b>高级配色:</b>支持前景/背景独立渐变、定位角独立着色</li>
        <li>非标准形状建议使用 H 级容错 + 大尺寸导出</li>
      </ul>
    </div>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
let qrCode = null, logoDataUrl = null, mediaDataUrl = null, currentShape = 'square';

// === Tab Switching ===
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
  document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
  document.querySelectorAll('.tab-pane').forEach(x => x.classList.remove('active'));
  t.classList.add('active');
  document.getElementById(t.dataset.t).classList.add('active');
}));

// === Shape Selection ===
document.querySelectorAll('.shape-opt').forEach(el => el.addEventListener('click', () => {
  document.querySelectorAll('.shape-opt').forEach(x => x.classList.remove('active'));
  el.classList.add('active');
  currentShape = el.dataset.shape;
  const frame = document.getElementById('qr-frame');
  frame.className = 'qr-frame shape-' + currentShape;
}));

// === Color Bindings ===
function bindColor(iid, hid) {
  const i = document.getElementById(iid), h = document.getElementById(hid);
  if(!h) return;
  i.addEventListener('input', () => { h.textContent = i.value; autoGen(); });
}
bindColor('fg-color','fg-hex'); bindColor('bg-color','bg-hex');
bindColor('corner-color','cc-hex'); bindColor('corner-dot-color','cdc-hex');

// === Range Bindings ===
function bindRange(iid, vid, suf='') {
  const i = document.getElementById(iid), v = document.getElementById(vid);
  i.addEventListener('input', () => { v.textContent = i.value + suf; autoGen(); });
}
bindRange('qr-size','size-val'); bindRange('qr-margin','margin-val');
bindRange('fg-grad-angle','fg-angle-val','°');
bindRange('logo-size','ls-val'); bindRange('logo-margin','lm-val');

// === Gradient Toggles ===
document.getElementById('use-fg-grad').addEventListener('change', function(){ 
  document.getElementById('fg-grad-opts').style.display = this.checked?'block':'none'; autoGen(); 
});
document.getElementById('use-bg-grad').addEventListener('change', function(){ 
  document.getElementById('bg-grad-opts').style.display = this.checked?'block':'none'; autoGen(); 
});
['fg-g1','fg-g2','fg-grad-angle','fg-grad-type'].forEach(id => 
  document.getElementById(id).addEventListener('input', () => { updateGradBar('fg'); autoGen(); })
);
['bg-g1','bg-g2','bg-grad-type'].forEach(id => 
  document.getElementById(id).addEventListener('input', () => { updateGradBar('bg'); autoGen(); })
);

function updateGradBar(prefix) {
  const c1 = document.getElementById(prefix+'-g1').value;
  const c2 = document.getElementById(prefix+'-g2').value;
  const type = document.getElementById(prefix+'-grad-type').value;
  const bar = document.getElementById(prefix+'-grad-bar');
  if(prefix==='fg') {
    const a = document.getElementById('fg-grad-angle').value;
    bar.style.background = `linear-gradient(${a}deg,${c1},${c2})`;
  } else {
    bar.style.background = type==='radial' ? `radial-gradient(circle,${c1},${c2})` : `linear-gradient(90deg,${c1},${c2})`;
  }
}
updateGradBar('fg'); updateGradBar('bg');

// === Auto-gen triggers ===
let debounce;
function autoGen() { clearTimeout(debounce); debounce = setTimeout(generateQR, 250); }
document.getElementById('qr-data').addEventListener('input', autoGen);
document.getElementById('ec-level').addEventListener('change', autoGen);
['dots-style','cs-style','cd-style'].forEach(id => document.getElementById(id).addEventListener('change', autoGen));
document.getElementById('logo-hide-bg').addEventListener('change', autoGen);

// === Logo Upload ===
const logoDrop = document.getElementById('logo-drop');
const logoInp = document.getElementById('logo-input');
logoDrop.addEventListener('click', () => logoInp.click());
logoDrop.addEventListener('dragover', e => { e.preventDefault(); logoDrop.style.borderColor='var(--accent)'; });
logoDrop.addEventListener('dragleave', () => logoDrop.style.borderColor='');
logoDrop.addEventListener('drop', e => { e.preventDefault(); logoDrop.style.borderColor=''; if(e.dataTransfer.files[0]) loadLogo(e.dataTransfer.files[0]); });
logoInp.addEventListener('change', e => { if(e.target.files[0]) loadLogo(e.target.files[0]); });

function loadLogo(file) {
  const r = new FileReader();
  r.onload = e => {
    logoDataUrl = e.target.result;
    document.getElementById('logo-thumb').src = logoDataUrl;
    document.getElementById('logo-preview-area').style.display = 'block';
    logoDrop.style.display = 'none';
    autoGen();
  };
  r.readAsDataURL(file);
}
window.removeLogo = function() {
  logoDataUrl = null;
  document.getElementById('logo-preview-area').style.display = 'none';
  logoDrop.style.display = 'block';
  logoInp.value = '';
  autoGen();
};

// === Media Upload ===
const mediaDrop = document.getElementById('media-drop');
const mediaInp = document.getElementById('media-input');
mediaDrop.addEventListener('click', () => mediaInp.click());
mediaDrop.addEventListener('dragover', e => { e.preventDefault(); mediaDrop.style.borderColor='var(--accent)'; });
mediaDrop.addEventListener('dragleave', () => mediaDrop.style.borderColor='');
mediaDrop.addEventListener('drop', e => { e.preventDefault(); mediaDrop.style.borderColor=''; if(e.dataTransfer.files[0]) loadMedia(e.dataTransfer.files[0]); });
mediaInp.addEventListener('change', e => { if(e.target.files[0]) loadMedia(e.target.files[0]); });

function loadMedia(file) {
  const warn = document.getElementById('media-warn');
  const maxSize = 2048; // 2KB safe limit for QR
  if(file.size > maxSize) {
    warn.style.display = 'block';
    warn.textContent = `⚠️ 文件 ${(file.size/1024).toFixed(1)}KB 超出安全限制 (${maxSize/1024}KB)。二维码可能过密无法扫描!建议使用更小的文件或缩短内容。`;
  } else {
    warn.style.display = 'none';
  }
  const r = new FileReader();
  r.onload = e => {
    mediaDataUrl = e.target.result;
    const isVideo = file.type.startsWith('video');
    const area = document.getElementById('media-preview-area');
    area.style.display = 'block';
    area.innerHTML = `
      <div class="media-preview">
        ${isVideo ? `<video src="${mediaDataUrl}" controls></video>` : `<audio src="${mediaDataUrl}" controls></audio>`}
        <div class="media-info">
          <div class="name">${file.name}</div>
          <div class="size">${(file.size/1024).toFixed(1)} KB · ${file.type}</div>
        </div>
        <button class="media-remove" onclick="removeMedia()">✕</button>
      </div>`;
    mediaDrop.style.display = 'none';
    // Set QR content to data URI so scanning opens media directly
    document.getElementById('qr-data').value = mediaDataUrl;
    autoGen();
  };
  r.readAsDataURL(file);
}
window.removeMedia = function() {
  mediaDataUrl = null;
  document.getElementById('media-preview-area').style.display = 'none';
  document.getElementById('media-warn').style.display = 'none';
  mediaDrop.style.display = 'block';
  mediaInp.value = '';
  document.getElementById('qr-data').value = 'https://github.com';
  autoGen();
};

// === Core Generate ===
function getOpts() {
  const o = {
    width: +document.getElementById('qr-size').value,
    height: +document.getElementById('qr-size').value,
    type: 'canvas',
    margin: +document.getElementById('qr-margin').value,
    data: document.getElementById('qr-data').value || 'Hello',
    qrOptions: { errorCorrectionLevel: document.getElementById('ec-level').value },
    dotsOptions: { type: document.getElementById('dots-style').value },
    cornersSquareOptions: { type: document.getElementById('cs-style').value, color: document.getElementById('corner-color').value },
    cornersDotOptions: { type: document.getElementById('cd-style').value, color: document.getElementById('corner-dot-color').value },
    backgroundOptions: { color: document.getElementById('bg-color').value },
    imageOptions: { crossOrigin:'anonymous', margin:+document.getElementById('logo-margin').value, imageSize:+document.getElementById('logo-size').value, hideBackgroundDots:document.getElementById('logo-hide-bg').checked }
  };
  // FG gradient
  if(document.getElementById('use-fg-grad').checked) {
    o.dotsOptions.gradient = {
      type: document.getElementById('fg-grad-type').value,
      rotation: +document.getElementById('fg-grad-angle').value * Math.PI/180,
      colorStops: [{offset:0,color:document.getElementById('fg-g1').value},{offset:1,color:document.getElementById('fg-g2').value}]
    };
  } else {
    o.dotsOptions.color = document.getElementById('fg-color').value;
  }
  // BG gradient
  if(document.getElementById('use-bg-grad').checked) {
    o.backgroundOptions = {
      gradient: {
        type: document.getElementById('bg-grad-type').value,
        rotation: 0,
        colorStops: [{offset:0,color:document.getElementById('bg-g1').value},{offset:1,color:document.getElementById('bg-g2').value}]
      }
    };
  }
  if(logoDataUrl) o.image = logoDataUrl;
  return o;
}

function generateQR() {
  const c = document.getElementById('qr-code');
  c.innerHTML = '';
  qrCode = new QRCodeStyling(getOpts());
  qrCode.append(c);
}

function dl(fmt) {
  if(!qrCode) return;
  qrCode.download({name:'qrcode',extension:fmt});
  toast(`已下载 ${fmt.toUpperCase()}`);
}

function toast(msg) {
  const t = document.getElementById('toast');
  t.textContent = msg; t.classList.add('show');
  setTimeout(() => t.classList.remove('show'), 2000);
}

// Init
generateQR();
</script>
</body>
</html>
        
编辑器加载中
预览
控制台