<!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>⚠️ 仅支持小文件 (<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>
index.html
index.html