未命名 9Vovkvedit 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>二维码自定义模拟器</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/qr-code-styling.js"></script>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
    min-height: 100vh; color: #e0e0e0;
  }
  .header {
    text-align: center; padding: 30px 20px 10px;
  }
  .header h1 {
    font-size: 2.2rem; background: linear-gradient(90deg, #00d2ff, #7b2ff7, #ff6fd8);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent;
    text-shadow: none; margin-bottom: 6px;
  }
  .header p { color: #8888aa; font-size: 0.95rem; }
  .container {
    display: flex; flex-wrap: wrap; gap: 24px;
    max-width: 1200px; margin: 20px auto; padding: 0 20px 40px;
    justify-content: center;
  }
  .panel {
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 16px; padding: 24px;
    backdrop-filter: blur(12px);
    flex: 1; min-width: 320px; max-width: 560px;
  }
  .panel-title {
    font-size: 1.1rem; font-weight: 600; margin-bottom: 18px;
    display: flex; align-items: center; gap: 8px;
    color: #c0c0ff;
  }
  .panel-title .icon { font-size: 1.3rem; }
  .form-group { margin-bottom: 16px; }
  .form-group label {
    display: block; font-size: 0.85rem; color: #9999bb;
    margin-bottom: 6px; font-weight: 500;
  }
  textarea, input[type="text"], input[type="number"], select {
    width: 100%; padding: 10px 14px;
    background: rgba(255,255,255,0.07);
    border: 1px solid rgba(255,255,255,0.12);
    border-radius: 10px; color: #e0e0e0;
    font-size: 0.95rem; outline: none;
    transition: border-color 0.3s, box-shadow 0.3s;
  }
  textarea:focus, input:focus, select:focus {
    border-color: #7b2ff7;
    box-shadow: 0 0 0 3px rgba(123,47,247,0.15);
  }
  textarea { resize: vertical; min-height: 70px; }
  select { cursor: pointer; appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%239999bb' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 12px center;
  }
  select option { background: #2a2a4a; color: #e0e0e0; }
  .row { display: flex; gap: 12px; }
  .row .form-group { flex: 1; }
  .color-group {
    display: flex; align-items: center; gap: 10px;
  }
  .color-group input[type="color"] {
    width: 44px; height: 38px; border: none; border-radius: 8px;
    cursor: pointer; background: none; padding: 0;
  }
  .color-group input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
  .color-group input[type="color"]::-webkit-color-swatch { border-radius: 6px; border: none; }
  .color-group .hex-display {
    font-family: 'Courier New', monospace; font-size: 0.9rem; color: #aaa;
  }
  input[type="range"] {
    width: 100%; height: 6px; -webkit-appearance: none;
    background: rgba(255,255,255,0.1); border-radius: 3px; outline: none;
  }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; width: 18px; height: 18px;
    background: linear-gradient(135deg, #7b2ff7, #00d2ff);
    border-radius: 50%; cursor: pointer; border: 2px solid #fff;
  }
  .range-row {
    display: flex; align-items: center; gap: 12px;
  }
  .range-row input[type="range"] { flex: 1; }
  .range-row .range-val {
    min-width: 42px; text-align: center;
    font-size: 0.85rem; color: #aaa;
    background: rgba(255,255,255,0.06);
    padding: 4px 8px; border-radius: 6px;
  }
  .checkbox-group {
    display: flex; align-items: center; gap: 8px; cursor: pointer;
  }
  .checkbox-group input[type="checkbox"] {
    width: 18px; height: 18px; accent-color: #7b2ff7; cursor: pointer;
  }
  .tab-bar {
    display: flex; gap: 4px; margin-bottom: 16px;
    background: rgba(255,255,255,0.04); border-radius: 10px; padding: 4px;
  }
  .tab-btn {
    flex: 1; padding: 8px; text-align: center;
    border: none; background: transparent; color: #8888aa;
    border-radius: 8px; cursor: pointer; font-size: 0.85rem;
    transition: all 0.3s;
  }
  .tab-btn.active {
    background: rgba(123,47,247,0.25); color: #d0d0ff;
  }
  .tab-content { display: none; }
  .tab-content.active { display: block; }
  .preview-area {
    display: flex; flex-direction: column; align-items: center; gap: 20px;
  }
  .qr-canvas-wrapper {
    background: #fff; border-radius: 16px; padding: 20px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.3);
    display: flex; align-items: center; justify-content: center;
    min-width: 260px; min-height: 260px;
    transition: transform 0.3s;
  }
  .qr-canvas-wrapper:hover { transform: scale(1.02); }
  #qr-code { display: flex; align-items: center; justify-content: center; }
  #qr-code canvas, #qr-code svg { max-width: 100%; }
  .btn-group {
    display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
  }
  .btn {
    padding: 10px 22px; border: none; border-radius: 10px;
    font-size: 0.9rem; cursor: pointer; font-weight: 600;
    transition: all 0.3s; display: flex; align-items: center; gap: 6px;
  }
  .btn-primary {
    background: linear-gradient(135deg, #7b2ff7, #5b1fd7);
    color: #fff;
  }
  .btn-primary:hover { box-shadow: 0 4px 20px rgba(123,47,247,0.4); transform: translateY(-1px); }
  .btn-success {
    background: linear-gradient(135deg, #00c853, #009624);
    color: #fff;
  }
  .btn-success:hover { box-shadow: 0 4px 20px rgba(0,200,83,0.4); transform: translateY(-1px); }
  .btn-outline {
    background: transparent; color: #aaa;
    border: 1px solid rgba(255,255,255,0.15);
  }
  .btn-outline:hover { border-color: #7b2ff7; color: #d0d0ff; }
  .preset-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
    gap: 8px; margin-top: 8px;
  }
  .preset-card {
    padding: 10px 6px; text-align: center;
    background: rgba(255,255,255,0.04);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 10px; cursor: pointer;
    transition: all 0.3s; font-size: 0.78rem;
  }
  .preset-card:hover {
    border-color: #7b2ff7; background: rgba(123,47,247,0.1);
    transform: translateY(-2px);
  }
  .preset-card .emoji { font-size: 1.5rem; display: block; margin-bottom: 4px; }
  .gradient-preview {
    width: 100%; height: 24px; border-radius: 6px;
    margin-top: 6px; border: 1px solid rgba(255,255,255,0.1);
  }
  .section-divider {
    border: none; border-top: 1px solid rgba(255,255,255,0.06);
    margin: 16px 0;
  }
  .logo-upload-area {
    border: 2px dashed rgba(255,255,255,0.12);
    border-radius: 10px; padding: 20px; text-align: center;
    cursor: pointer; transition: all 0.3s;
    color: #777; font-size: 0.85rem;
  }
  .logo-upload-area:hover {
    border-color: #7b2ff7; color: #aaa;
    background: rgba(123,47,247,0.05);
  }
  .logo-preview {
    max-width: 60px; max-height: 60px;
    border-radius: 8px; margin-top: 8px;
  }
  .toast {
    position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
    background: rgba(123,47,247,0.9); color: #fff;
    padding: 12px 28px; border-radius: 10px;
    font-size: 0.9rem; opacity: 0; transition: opacity 0.4s;
    pointer-events: none; z-index: 999;
    backdrop-filter: blur(8px);
  }
  .toast.show { opacity: 1; }
  .history-list {
    max-height: 200px; overflow-y: auto; margin-top: 8px;
  }
  .history-item {
    display: flex; align-items: center; gap: 10px;
    padding: 8px; border-radius: 8px;
    background: rgba(255,255,255,0.03);
    margin-bottom: 6px; cursor: pointer;
    transition: background 0.2s;
  }
  .history-item:hover { background: rgba(255,255,255,0.07); }
  .history-item .thumb {
    width: 36px; height: 36px; border-radius: 6px;
    background: #fff; display: flex; align-items: center;
    justify-content: center; flex-shrink: 0; overflow: hidden;
  }
  .history-item .thumb canvas { width: 100%; height: 100%; }
  .history-item .info { flex: 1; overflow: hidden; }
  .history-item .info .text {
    font-size: 0.82rem; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  .history-item .info .time { font-size: 0.72rem; color: #666; }
  .history-item .del-btn {
    background: none; border: none; color: #666;
    cursor: pointer; font-size: 1rem; padding: 4px;
  }
  .history-item .del-btn:hover { color: #f44; }
  @media (max-width: 700px) {
    .container { flex-direction: column; }
    .panel { max-width: 100%; }
    .header h1 { font-size: 1.6rem; }
  }
  ::-webkit-scrollbar { width: 6px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
</style>
</head>
<body>

<div class="header">
  <h1>⬛ 二维码自定义模拟器</h1>
  <p>自定义样式 · 渐变配色 · Logo嵌入 · 多格式导出</p>
</div>

<div class="container">
  <!-- 左侧:配置面板 -->
  <div class="panel">
    <div class="panel-title"><span class="icon">⚙️</span> 基础设置</div>

    <div class="form-group">
      <label>二维码内容</label>
      <textarea id="qr-data" placeholder="输入文本、网址、WiFi信息等...">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-row">
          <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>边距 (margin)</label>
      <div class="range-row">
        <input type="range" id="qr-margin" min="0" max="50" value="10">
        <span class="range-val" id="margin-val">10</span>
      </div>
    </div>

    <hr class="section-divider">

    <!-- Tab切换:颜色 / 样式 / Logo / 预设 -->
    <div class="tab-bar">
      <button class="tab-btn active" data-tab="tab-color">🎨 颜色</button>
      <button class="tab-btn" data-tab="tab-style">🔷 样式</button>
      <button class="tab-btn" data-tab="tab-logo">🖼️ Logo</button>
      <button class="tab-btn" data-tab="tab-preset">✨ 预设</button>
    </div>

    <!-- 颜色 Tab -->
    <div class="tab-content active" id="tab-color">
      <div class="form-group">
        <label>前景色(码点)</label>
        <div class="color-group">
          <input type="color" id="fg-color" value="#000000">
          <span class="hex-display" id="fg-hex">#000000</span>
        </div>
      </div>
      <div class="form-group">
        <label>背景色</label>
        <div class="color-group">
          <input type="color" id="bg-color" value="#ffffff">
          <span class="hex-display" id="bg-hex">#ffffff</span>
        </div>
      </div>
      <hr class="section-divider">
      <div class="form-group">
        <label class="checkbox-group">
          <input type="checkbox" id="use-gradient"> 启用渐变前景
        </label>
      </div>
      <div id="gradient-options" style="display:none;">
        <div class="form-group">
          <label>渐变类型</label>
          <select id="gradient-type">
            <option value="linear">线性渐变</option>
            <option value="radial">径向渐变</option>
          </select>
        </div>
        <div class="row">
          <div class="form-group">
            <label>渐变起始色</label>
            <div class="color-group">
              <input type="color" id="grad-start" value="#7b2ff7">
            </div>
          </div>
          <div class="form-group">
            <label>渐变结束色</label>
            <div class="color-group">
              <input type="color" id="grad-end" value="#00d2ff">
            </div>
          </div>
        </div>
        <div class="form-group">
          <label>渐变角度</label>
          <div class="range-row">
            <input type="range" id="grad-angle" min="0" max="360" value="45">
            <span class="range-val" id="angle-val">45°</span>
          </div>
        </div>
        <div class="gradient-preview" id="grad-preview"></div>
      </div>
      <hr class="section-divider">
      <div class="form-group">
        <label class="checkbox-group">
          <input type="checkbox" id="use-bg-gradient"> 启用渐变背景
        </label>
      </div>
      <div id="bg-gradient-options" style="display:none;">
        <div class="row">
          <div class="form-group">
            <label>背景渐变起始</label>
            <div class="color-group">
              <input type="color" id="bg-grad-start" value="#f0f0f0">
            </div>
          </div>
          <div class="form-group">
            <label>背景渐变结束</label>
            <div class="color-group">
              <input type="color" id="bg-grad-end" value="#e0e0ff">
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 样式 Tab -->
    <div class="tab-content" id="tab-style">
      <div class="form-group">
        <label>码点样式 (Dots Style)</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>定位角样式 (Corner Square)</label>
        <select id="corner-square-style">
          <option value="square" selected>■ 方形</option>
          <option value="dot">● 圆形</option>
          <option value="extra-rounded">⬮ 超圆角</option>
        </select>
      </div>
      <div class="form-group">
        <label>定位角内点样式 (Corner Dot)</label>
        <select id="corner-dot-style">
          <option value="square" selected>■ 方形</option>
          <option value="dot">● 圆形</option>
        </select>
      </div>
      <div class="form-group">
        <label>定位角颜色</label>
        <div class="color-group">
          <input type="color" id="corner-color" value="#000000">
          <span class="hex-display" id="corner-hex">#000000</span>
        </div>
      </div>
      <div class="form-group">
        <label>定位角内点颜色</label>
        <div class="color-group">
          <input type="color" id="corner-dot-color" value="#000000">
          <span class="hex-display" id="corner-dot-hex">#000000</span>
        </div>
      </div>
    </div>

    <!-- Logo Tab -->
    <div class="tab-content" id="tab-logo">
      <div class="form-group">
        <label>上传 Logo 图片</label>
        <div class="logo-upload-area" id="logo-upload">
          📁 点击或拖拽图片到此处
          <input type="file" id="logo-input" accept="image/*" style="display:none;">
        </div>
        <div id="logo-preview-wrap" style="display:none; text-align:center; margin-top:10px;">
          <img id="logo-preview" class="logo-preview">
          <br>
          <button class="btn btn-outline" style="margin-top:8px; font-size:0.78rem; padding:6px 14px;" onclick="removeLogo()">移除 Logo</button>
        </div>
      </div>
      <div class="form-group">
        <label>Logo 尺寸</label>
        <div class="range-row">
          <input type="range" id="logo-size" min="0.1" max="0.5" step="0.05" value="0.3">
          <span class="range-val" id="logo-size-val">0.3</span>
        </div>
      </div>
      <div class="form-group">
        <label>Logo 边距</label>
        <div class="range-row">
          <input type="range" id="logo-margin" min="0" max="20" value="5">
          <span class="range-val" id="logo-margin-val">5</span>
        </div>
      </div>
      <div class="form-group">
        <label class="checkbox-group">
          <input type="checkbox" id="logo-hide-bg" checked> 隐藏Logo下方码点
        </label>
      </div>
    </div>

    <!-- 预设 Tab -->
    <div class="tab-content" id="tab-preset">
      <div class="preset-grid">
        <div class="preset-card" onclick="applyPreset('classic')">
          <span class="emoji">⬛</span>经典黑白
        </div>
        <div class="preset-card" onclick="applyPreset('ocean')">
          <span class="emoji">🌊</span>海洋蓝
        </div>
        <div class="preset-card" onclick="applyPreset('sunset')">
          <span class="emoji">🌅</span>日落橙
        </div>
        <div class="preset-card" onclick="applyPreset('forest')">
          <span class="emoji">🌲</span>森林绿
        </div>
        <div class="preset-card" onclick="applyPreset('neon')">
          <span class="emoji">💜</span>霓虹紫
        </div>
        <div class="preset-card" onclick="applyPreset('rose')">
          <span class="emoji">🌸</span>玫瑰粉
        </div>
        <div class="preset-card" onclick="applyPreset('dark')">
          <span class="emoji">🖤</span>暗黑模式
        </div>
        <div class="preset-card" onclick="applyPreset('gold')">
          <span class="emoji">🏆</span>金色奢华
        </div>
        <div class="preset-card" onclick="applyPreset('candy')">
          <span class="emoji">🍬</span>糖果渐变
        </div>
        <div class="preset-card" onclick="applyPreset('retro')">
          <span class="emoji">📺</span>复古棕
        </div>
      </div>
    </div>
  </div>

  <!-- 右侧:预览面板 -->
  <div class="panel" style="max-width: 420px; min-width: 300px;">
    <div class="panel-title"><span class="icon">👁️</span> 实时预览</div>
    <div class="preview-area">
      <div class="qr-canvas-wrapper" id="qr-wrapper">
        <div id="qr-code"></div>
      </div>
      <div class="btn-group">
        <button class="btn btn-primary" onclick="generateQR()">🔄 生成</button>
        <button class="btn btn-success" onclick="downloadQR('png')">📥 PNG</button>
        <button class="btn btn-success" onclick="downloadQR('jpeg')">📥 JPEG</button>
        <button class="btn btn-success" onclick="downloadQR('svg')">📥 SVG</button>
      </div>
      <button class="btn btn-outline" onclick="saveToHistory()" style="font-size:0.82rem;">💾 保存到历史</button>
    </div>

    <hr class="section-divider">
    <div class="panel-title" style="font-size:0.95rem;"><span class="icon">📋</span> 历史记录</div>
    <div class="history-list" id="history-list">
      <p style="color:#555; font-size:0.82rem; text-align:center;">暂无记录</p>
    </div>
    <button class="btn btn-outline" style="width:100%; margin-top:8px; font-size:0.8rem; padding:6px;" onclick="clearHistory()">🗑️ 清空历史</button>
  </div>
</div>

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

<script>
let qrCode = null;
let logoDataUrl = null;
let history = [];

// --- Tab 切换 ---
document.querySelectorAll('.tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById(btn.dataset.tab).classList.add('active');
  });
});

// --- 颜色联动 ---
function bindColor(inputId, hexId) {
  const inp = document.getElementById(inputId);
  const hex = document.getElementById(hexId);
  if (!hex) return;
  inp.addEventListener('input', () => { hex.textContent = inp.value; autoGenerate(); });
}
bindColor('fg-color', 'fg-hex');
bindColor('bg-color', 'bg-hex');
bindColor('corner-color', 'corner-hex');
bindColor('corner-dot-color', 'corner-dot-hex');

// --- Range 联动 ---
function bindRange(inputId, valId, suffix='') {
  const inp = document.getElementById(inputId);
  const val = document.getElementById(valId);
  inp.addEventListener('input', () => { val.textContent = inp.value + suffix; autoGenerate(); });
}
bindRange('qr-size', 'size-val');
bindRange('qr-margin', 'margin-val');
bindRange('grad-angle', 'angle-val', '°');
bindRange('logo-size', 'logo-size-val');
bindRange('logo-margin', 'logo-margin-val');

// --- 渐变预览 ---
document.getElementById('use-gradient').addEventListener('change', function() {
  document.getElementById('gradient-options').style.display = this.checked ? 'block' : 'none';
  autoGenerate();
});
document.getElementById('use-bg-gradient').addEventListener('change', function() {
  document.getElementById('bg-gradient-options').style.display = this.checked ? 'block' : 'none';
  autoGenerate();
});
['grad-start','grad-end','grad-angle','gradient-type','bg-grad-start','bg-grad-end'].forEach(id => {
  document.getElementById(id).addEventListener('input', () => { updateGradPreview(); autoGenerate(); });
});
function updateGradPreview() {
  const s = document.getElementById('grad-start').value;
  const e = document.getElementById('grad-end').value;
  const a = document.getElementById('grad-angle').value;
  document.getElementById('grad-preview').style.background =
    `linear-gradient(${a}deg, ${s}, ${e})`;
}
updateGradPreview();

// --- Logo 上传 ---
const logoUpload = document.getElementById('logo-upload');
const logoInput = document.getElementById('logo-input');
logoUpload.addEventListener('click', () => logoInput.click());
logoUpload.addEventListener('dragover', e => { e.preventDefault(); logoUpload.style.borderColor = '#7b2ff7'; });
logoUpload.addEventListener('dragleave', () => { logoUpload.style.borderColor = ''; });
logoUpload.addEventListener('drop', e => {
  e.preventDefault(); logoUpload.style.borderColor = '';
  if (e.dataTransfer.files[0]) handleLogoFile(e.dataTransfer.files[0]);
});
logoInput.addEventListener('change', e => { if (e.target.files[0]) handleLogoFile(e.target.files[0]); });

function handleLogoFile(file) {
  const reader = new FileReader();
  reader.onload = e => {
    logoDataUrl = e.target.result;
    document.getElementById('logo-preview').src = logoDataUrl;
    document.getElementById('logo-preview-wrap').style.display = 'block';
    logoUpload.style.display = 'none';
    autoGenerate();
  };
  reader.readAsDataURL(file);
}
function removeLogo() {
  logoDataUrl = null;
  document.getElementById('logo-preview-wrap').style.display = 'none';
  logoUpload.style.display = 'block';
  logoInput.value = '';
  autoGenerate();
}

// --- 自动更新 ---
let debounceTimer;
function autoGenerate() {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(generateQR, 300);
}
document.getElementById('qr-data').addEventListener('input', autoGenerate);
document.getElementById('ec-level').addEventListener('change', autoGenerate);
['dots-style','corner-square-style','corner-dot-style'].forEach(id => {
  document.getElementById(id).addEventListener('change', autoGenerate);
});
document.getElementById('logo-hide-bg').addEventListener('change', autoGenerate);

// --- 核心:生成二维码 ---
function getOptions() {
  const opts = {
    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('corner-square-style').value,
      color: document.getElementById('corner-color').value
    },
    cornersDotOptions: {
      type: document.getElementById('corner-dot-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
    }
  };

  // 前景渐变
  if (document.getElementById('use-gradient').checked) {
    const gs = document.getElementById('grad-start').value;
    const ge = document.getElementById('grad-end').value;
    const ga = +document.getElementById('grad-angle').value;
    const gt = document.getElementById('gradient-type').value;
    opts.dotsOptions.gradient = {
      type: gt,
      rotation: ga * Math.PI / 180,
      colorStops: [
        { offset: 0, color: gs },
        { offset: 1, color: ge }
      ]
    };
  } else {
    opts.dotsOptions.color = document.getElementById('fg-color').value;
  }

  // 背景渐变
  if (document.getElementById('use-bg-gradient').checked) {
    opts.backgroundOptions = {
      gradient: {
        type: 'linear',
        rotation: 0,
        colorStops: [
          { offset: 0, color: document.getElementById('bg-grad-start').value },
          { offset: 1, color: document.getElementById('bg-grad-end').value }
        ]
      }
    };
  }

  // Logo
  if (logoDataUrl) {
    opts.image = logoDataUrl;
  }

  return opts;
}

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

// --- 下载 ---
function downloadQR(format) {
  if (!qrCode) return;
  qrCode.download({ name: 'qrcode', extension: format });
  showToast(`已下载 ${format.toUpperCase()} 文件`);
}

// --- 预设 ---
const presets = {
  classic: {
    fg:'#000000', bg:'#ffffff', dots:'square', cs:'square', cd:'square',
    cc:'#000000', cdc:'#000000', gradient:false, bgGradient:false
  },
  ocean: {
    fg:'#0077b6', bg:'#e8f4f8', dots:'rounded', cs:'extra-rounded', cd:'dot',
    cc:'#0077b6', cdc:'#00b4d8', gradient:true, gs:'#0077b6', ge:'#00b4d8', ga:135, gt:'linear', bgGradient:false
  },
  sunset: {
    fg:'#e63946', bg:'#fff8f0', dots:'dots', cs:'dot', cd:'dot',
    cc:'#e63946', cdc:'#f4a261', gradient:true, gs:'#e63946', ge:'#f4a261', ga:90, gt:'linear', bgGradient:false
  },
  forest: {
    fg:'#2d6a4f', bg:'#f0fff4', dots:'classy', cs:'square', cd:'square',
    cc:'#2d6a4f', cdc:'#40916c', gradient:false, bgGradient:false
  },
  neon: {
    fg:'#7b2ff7', bg:'#0a0a1a', dots:'dot', cs:'dot', cd:'dot',
    cc:'#b14dff', cdc:'#00d2ff', gradient:true, gs:'#7b2ff7', ge:'#00d2ff', ga:45, gt:'linear', bgGradient:false
  },
  rose: {
    fg:'#d63384', bg:'#fff0f5', dots:'extra-rounded', cs:'extra-rounded', cd:'dot',
    cc:'#d63384', cdc:'#e91e9c', gradient:true, gs:'#d63384', ge:'#ff6fd8', ga:180, gt:'radial', bgGradient:false
  },
  dark: {
    fg:'#e0e0e0', bg:'#1a1a2e', dots:'rounded', cs:'extra-rounded', cd:'dot',
    cc:'#e0e0e0', cdc:'#888', gradient:false, bgGradient:false
  },
  gold: {
    fg:'#b8860b', bg:'#fffdf0', dots:'classy-rounded', cs:'extra-rounded', cd:'dot',
    cc:'#b8860b', cdc:'#daa520', gradient:true, gs:'#b8860b', ge:'#ffd700', ga:90, gt:'linear', bgGradient:false
  },
  candy: {
    fg:'#ff6fd8', bg:'#fff5fc', dots:'dots', cs:'dot', cd:'dot',
    cc:'#7b2ff7', cdc:'#00d2ff', gradient:true, gs:'#ff6fd8', ge:'#7b2ff7', ga:45, gt:'linear', bgGradient:false
  },
  retro: {
    fg:'#5c4033', bg:'#f5f0e8', dots:'square', cs:'square', cd:'square',
    cc:'#5c4033', cdc:'#8b6914', gradient:false, bgGradient:false
  }
};

function applyPreset(name) {
  const p = presets[name];
  document.getElementById('fg-color').value = p.fg;
  document.getElementById('fg-hex').textContent = p.fg;
  document.getElementById('bg-color').value = p.bg;
  document.getElementById('bg-hex').textContent = p.bg;
  document.getElementById('dots-style').value = p.dots;
  document.getElementById('corner-square-style').value = p.cs;
  document.getElementById('corner-dot-style').value = p.cd;
  document.getElementById('corner-color').value = p.cc;
  document.getElementById('corner-hex').textContent = p.cc;
  document.getElementById('corner-dot-color').value = p.cdc;
  document.getElementById('corner-dot-hex').textContent = p.cdc;
  document.getElementById('use-gradient').checked = p.gradient;
  document.getElementById('gradient-options').style.display = p.gradient ? 'block' : 'none';
  if (p.gradient) {
    document.getElementById('grad-start').value = p.gs;
    document.getElementById('grad-end').value = p.ge;
    document.getElementById('grad-angle').value = p.ga;
    document.getElementById('angle-val').textContent = p.ga + '°';
    document.getElementById('gradient-type').value = p.gt;
    updateGradPreview();
  }
  document.getElementById('use-bg-gradient').checked = p.bgGradient || false;
  document.getElementById('bg-gradient-options').style.display = p.bgGradient ? 'block' : 'none';
  autoGenerate();
  showToast(`已应用预设:${name}`);
}

// --- 历史记录 ---
function saveToHistory() {
  const canvas = document.querySelector('#qr-code canvas');
  if (!canvas) return;
  const dataUrl = canvas.toDataURL('image/png');
  history.unshift({
    data: document.getElementById('qr-data').value,
    img: dataUrl,
    time: new Date().toLocaleTimeString(),
    opts: JSON.stringify(getOptions())
  });
  if (history.length > 20) history.pop();
  renderHistory();
  showToast('已保存到历史记录');
}

function renderHistory() {
  const list = document.getElementById('history-list');
  if (history.length === 0) {
    list.innerHTML = '<p style="color:#555;font-size:0.82rem;text-align:center;">暂无记录</p>';
    return;
  }
  list.innerHTML = history.map((h, i) => `
    <div class="history-item" onclick="loadHistory(${i})">
      <div class="thumb"><img src="${h.img}" style="width:100%;height:100%;object-fit:contain;"></div>
      <div class="info">
        <div class="text">${h.data || '(空)'}</div>
        <div class="time">${h.time}</div>
      </div>
      <button class="del-btn" onclick="event.stopPropagation(); deleteHistory(${i})">✕</button>
    </div>
  `).join('');
}

function loadHistory(i) {
  const h = history[i];
  try {
    const opts = JSON.parse(h.opts);
    // Restore options to UI
    document.getElementById('qr-data').value = h.data;
    document.getElementById('qr-size').value = opts.width;
    document.getElementById('size-val').textContent = opts.width;
    document.getElementById('qr-margin').value = opts.margin;
    document.getElementById('margin-val').textContent = opts.margin;
    document.getElementById('ec-level').value = opts.qrOptions.errorCorrectionLevel;
    document.getElementById('dots-style').value = opts.dotsOptions.type;
    document.getElementById('corner-square-style').value = opts.cornersSquareOptions.type;
    document.getElementById('corner-dot-style').value = opts.cornersDotOptions.type;
    document.getElementById('corner-color').value = opts.cornersSquareOptions.color;
    document.getElementById('corner-hex').textContent = opts.cornersSquareOptions.color;
    document.getElementById('corner-dot-color').value = opts.cornersDotOptions.color;
    document.getElementById('corner-dot-hex').textContent = opts.cornersDotOptions.color;

    if (opts.dotsOptions.gradient) {
      document.getElementById('use-gradient').checked = true;
      document.getElementById('gradient-options').style.display = 'block';
      const g = opts.dotsOptions.gradient;
      document.getElementById('grad-start').value = g.colorStops[0].color;
      document.getElementById('grad-end').value = g.colorStops[1].color;
      const angle = Math.round(g.rotation * 180 / Math.PI);
      document.getElementById('grad-angle').value = angle;
      document.getElementById('angle-val').textContent = angle + '°';
      document.getElementById('gradient-type').value = g.type;
      updateGradPreview();
    } else {
      document.getElementById('use-gradient').checked = false;
      document.getElementById('gradient-options').style.display = 'none';
      document.getElementById('fg-color').value = opts.dotsOptions.color;
      document.getElementById('fg-hex').textContent = opts.dotsOptions.color;
    }

    if (opts.backgroundOptions.gradient) {
      document.getElementById('use-bg-gradient').checked = true;
      document.getElementById('bg-gradient-options').style.display = 'block';
      document.getElementById('bg-grad-start').value = opts.backgroundOptions.gradient.colorStops[0].color;
      document.getElementById('bg-grad-end').value = opts.backgroundOptions.gradient.colorStops[1].color;
    } else {
      document.getElementById('use-bg-gradient').checked = false;
      document.getElementById('bg-gradient-options').style.display = 'none';
      document.getElementById('bg-color').value = opts.backgroundOptions.color;
      document.getElementById('bg-hex').textContent = opts.backgroundOptions.color;
    }

    if (opts.image) {
      logoDataUrl = opts.image;
      document.getElementById('logo-preview').src = logoDataUrl;
      document.getElementById('logo-preview-wrap').style.display = 'block';
      logoUpload.style.display = 'none';
      document.getElementById('logo-size').value = opts.imageOptions.imageSize;
      document.getElementById('logo-size-val').textContent = opts.imageOptions.imageSize;
      document.getElementById('logo-margin').value = opts.imageOptions.margin;
      document.getElementById('logo-margin-val').textContent = opts.imageOptions.margin;
      document.getElementById('logo-hide-bg').checked = opts.imageOptions.hideBackgroundDots;
    }

    generateQR();
    showToast('已加载历史记录');
  } catch(e) { console.error(e); }
}

function deleteHistory(i) {
  history.splice(i, 1);
  renderHistory();
}

function clearHistory() {
  history = [];
  renderHistory();
  showToast('历史已清空');
}

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

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