<!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>
index.html
index.html