feat: web 端 GUI 风格界面 + SVG 支持 + 预览清晰度修复
- index.html: 完整复刻 GUI 三栏布局(模式/预览/设置) 7 种编码模式切换,实时防抖预览,下载 PNG/SVG/复制 - main.rs: API 新增 fmt=svg 参数,预览用 PNG size=8 避免拉伸模糊 - Dockerfile: 多阶段构建(alpine),部署就绪 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+13
-7
@@ -19,6 +19,9 @@ struct QrParams {
|
||||
margin: u8,
|
||||
#[serde(default = "default_size")]
|
||||
size: u8,
|
||||
/// fmt=svg 返回 SVG,否则返回 PNG
|
||||
#[serde(default)]
|
||||
fmt: String,
|
||||
}
|
||||
|
||||
fn default_level() -> String { "M".into() }
|
||||
@@ -40,7 +43,7 @@ async fn index() -> Html<&'static str> {
|
||||
Html(include_str!("templates/index.html"))
|
||||
}
|
||||
|
||||
/// QR 码生成 API → PNG 图片
|
||||
/// QR 码生成 API → PNG 或 SVG
|
||||
async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
|
||||
let level = match parse_level(¶ms.level) {
|
||||
Ok(l) => l,
|
||||
@@ -58,12 +61,15 @@ async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
|
||||
Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(),
|
||||
};
|
||||
|
||||
let png = match qr.to_png_bytes(params.size) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
([(header::CONTENT_TYPE, "image/png")], png).into_response()
|
||||
if params.fmt == "svg" {
|
||||
let svg = qr.to_svg();
|
||||
([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response()
|
||||
} else {
|
||||
match qr.to_png_bytes(params.size) {
|
||||
Ok(b) => ([(header::CONTENT_TYPE, "image/png")], b).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
||||
+232
-87
@@ -6,117 +6,262 @@
|
||||
<title>QRGen Web</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) { body { background: #1a1a2e; color: #e0e0e0; } }
|
||||
.card {
|
||||
background: #fff; border-radius: 16px; padding: 32px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,.08); max-width: 420px; width: 90%;
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) { .card { background: #16213e; } }
|
||||
h1 { font-size: 20px; font-weight: 600; text-align: center; }
|
||||
textarea, input, select {
|
||||
width: 100%; padding: 10px 12px; border: 1px solid #ddd;
|
||||
border-radius: 8px; font-size: 14px; background: #fafafa;
|
||||
font-family: inherit;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
textarea, input, select { background: #0f3460; border-color: #1a1a2e; color: #e0e0e0; }
|
||||
}
|
||||
textarea { resize: vertical; min-height: 80px; }
|
||||
.row { display: flex; gap: 12px; align-items: center; }
|
||||
.row label { font-size: 13px; white-space: nowrap; min-width: 60px; }
|
||||
button {
|
||||
padding: 10px 24px; border: none; border-radius: 8px;
|
||||
background: #2563eb; color: #fff; font-size: 14px; font-weight: 500;
|
||||
cursor: pointer; transition: background .15s;
|
||||
}
|
||||
button:hover { background: #1d4ed8; }
|
||||
button:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.preview {
|
||||
display: flex; justify-content: center; padding: 16px;
|
||||
background: #fff; border-radius: 12px; min-height: 200px;
|
||||
align-items: center;
|
||||
}
|
||||
.preview img { max-width: 240px; }
|
||||
.info { font-size: 12px; color: #888; text-align: center; }
|
||||
.actions { display: flex; gap: 8px; }
|
||||
.actions button { flex: 1; }
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;height:100vh;display:flex;flex-direction:column;background:#f3f4f6;overflow:hidden}
|
||||
@media(prefers-color-scheme:dark){body{background:#0a0a0f;color:#e0e0e0}}
|
||||
|
||||
/* 标题栏 */
|
||||
.titlebar{height:40px;display:flex;align-items:center;padding:0 16px;background:rgba(255,255,255,.8);border-bottom:1px solid #e5e7eb;backdrop-filter:blur(20px);font-size:13px;font-weight:600;color:#374151}
|
||||
@media(prefers-color-scheme:dark){.titlebar{background:rgba(15,15,25,.8);border-color:#1e1e30;color:#d1d5db}}
|
||||
|
||||
/* 主体三栏 */
|
||||
.main{flex:1;display:flex;overflow:hidden}
|
||||
|
||||
/* 左栏 — 模式 */
|
||||
.sidebar-l{width:160px;display:flex;flex-direction:column;padding:8px;gap:2px;background:rgba(255,255,255,.5);border-right:1px solid #e5e7eb;overflow-y:auto}
|
||||
@media(prefers-color-scheme:dark){.sidebar-l{background:rgba(15,15,25,.5);border-color:#1e1e30}}
|
||||
.sidebar-l .label{font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;padding:4px 8px;margin-bottom:4px}
|
||||
.sidebar-l button{all:unset;display:block;padding:8px 12px;border-radius:8px;font-size:13px;cursor:pointer;transition:all .15s;color:#4b5563}
|
||||
@media(prefers-color-scheme:dark){.sidebar-l button{color:#9ca3af}}
|
||||
.sidebar-l button:hover{background:#f3f4f6}
|
||||
@media(prefers-color-scheme:dark){.sidebar-l button:hover{background:#1e1e30}}
|
||||
.sidebar-l button.active{background:linear-gradient(135deg,#2563eb,#06b6d4);color:#fff;box-shadow:0 2px 8px rgba(37,99,235,.25)}
|
||||
|
||||
/* 中栏 — 预览 */
|
||||
.center{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px;padding:16px}
|
||||
.preview-box{width:240px;height:240px;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.06)}
|
||||
.preview-box img{width:216px;height:216px}
|
||||
.preview-box .placeholder{color:#9ca3af;font-size:13px}
|
||||
.preview-box .loading{color:#9ca3af;font-size:13px;animation:pulse 1.5s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.preview-info{display:flex;gap:12px;font-size:11px;color:#9ca3af}
|
||||
|
||||
/* 右栏 — 设置 */
|
||||
.sidebar-r{width:200px;display:flex;flex-direction:column;padding:12px;gap:12px;background:rgba(255,255,255,.5);border-left:1px solid #e5e7eb;overflow-y:auto}
|
||||
@media(prefers-color-scheme:dark){.sidebar-r{background:rgba(15,15,25,.5);border-color:#1e1e30}}
|
||||
.sidebar-r .label{font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px}
|
||||
.sidebar-r select,.sidebar-r input[type=range]{width:100%;margin-top:4px;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;background:#fff;outline:none}
|
||||
@media(prefers-color-scheme:dark){.sidebar-r select,.sidebar-r input[type=range]{background:#1a1a2e;border-color:#2a2a40;color:#d1d5db}}
|
||||
.sidebar-r select:focus,.sidebar-r input:focus{ring:2px solid rgba(37,99,235,.3)}
|
||||
.sidebar-r .slider-row{font-size:11px;color:#6b7280}
|
||||
.sidebar-r button{width:100%;padding:8px;border:none;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;color:#fff;transition:all .15s}
|
||||
.sidebar-r button:disabled{opacity:.35;cursor:not-allowed}
|
||||
.btn-copy{background:#2563eb}.btn-copy:hover{background:#1d4ed8}
|
||||
.btn-png{background:#16a34a}.btn-png:hover{background:#15803d}
|
||||
.btn-svg{background:#9333ea}.btn-svg:hover{background:#7e22ce}
|
||||
|
||||
/* 底部输入 */
|
||||
.bottom{height:88px;border-top:1px solid #e5e7eb;background:rgba(255,255,255,.8);backdrop-filter:blur(20px);padding:12px 16px;display:flex;align-items:center;gap:8px}
|
||||
@media(prefers-color-scheme:dark){.bottom{background:rgba(15,15,25,.8);border-color:#1e1e30}}
|
||||
.bottom input,.bottom textarea,.bottom select{flex:1;min-width:0;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;outline:none;background:#fafafa;font-family:inherit}
|
||||
@media(prefers-color-scheme:dark){.bottom input,.bottom textarea,.bottom select{background:#1a1a2e;border-color:#2a2a40;color:#d1d5db}}
|
||||
.bottom textarea{resize:none;height:100%}
|
||||
.bottom input:focus,.bottom textarea:focus{border-color:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,.15)}
|
||||
.bottom input[type=checkbox]{flex:none;width:auto}
|
||||
.bottom .wifi-check{display:flex;align-items:center;gap:4px;font-size:12px;color:#6b7280;white-space:nowrap}
|
||||
.error{font-size:11px;color:#ef4444;background:rgba(239,68,68,.1);padding:6px 8px;border-radius:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>QRGen Web</h1>
|
||||
<textarea id="text" placeholder="输入要编码的内容…"></textarea>
|
||||
<div class="row">
|
||||
<label>纠错级别</label>
|
||||
<select id="level">
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<div class="titlebar">QRGen</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="main">
|
||||
<!-- 左栏 — 编码模式 -->
|
||||
<div class="sidebar-l">
|
||||
<div class="label">编码模式</div>
|
||||
<button class="active" data-mode="text">📝 文本</button>
|
||||
<button data-mode="url">🔗 URL</button>
|
||||
<button data-mode="wifi">📶 WiFi</button>
|
||||
<button data-mode="vcard">👤 名片</button>
|
||||
<button data-mode="email">📧 Email</button>
|
||||
<button data-mode="phone">📞 电话</button>
|
||||
<button data-mode="sms">💬 SMS</button>
|
||||
</div>
|
||||
|
||||
<!-- 中栏 — QR 预览 -->
|
||||
<div class="center">
|
||||
<div class="preview-box" id="preview">
|
||||
<span class="placeholder" id="previewPlaceholder">输入内容生成 QR 码</span>
|
||||
<img id="previewImg" alt="QR" style="display:none">
|
||||
<span class="loading" id="previewLoading" style="display:none">生成中…</span>
|
||||
</div>
|
||||
<div class="preview-info" id="previewInfo"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏 — 设置 + 导出 -->
|
||||
<div class="sidebar-r">
|
||||
<div class="label">导出选项</div>
|
||||
<div>
|
||||
<div class="slider-row">纠错级别</div>
|
||||
<select id="cfgLevel">
|
||||
<option value="L">L — 7%</option>
|
||||
<option value="M" selected>M — 15%</option>
|
||||
<option value="Q">Q — 25%</option>
|
||||
<option value="H">H — 30%</option>
|
||||
</select>
|
||||
<label>边距</label>
|
||||
<input id="margin" type="number" value="4" min="1" max="20" style="width:64px">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>模块大小</label>
|
||||
<input id="size" type="range" value="8" min="2" max="20" oninput="document.getElementById('sizeVal').textContent=this.value">
|
||||
<span id="sizeVal" style="font-size:13px">8</span>px
|
||||
<div>
|
||||
<div class="slider-row">模块大小: <span id="sizeVal">8</span>px</div>
|
||||
<input type="range" id="cfgSize" min="2" max="20" value="8">
|
||||
</div>
|
||||
<div class="preview" id="preview">
|
||||
<span style="color:#999">输入内容生成 QR 码</span>
|
||||
<div>
|
||||
<div class="slider-row">边距: <span id="marginVal">4</span></div>
|
||||
<input type="range" id="cfgMargin" min="1" max="10" value="4">
|
||||
</div>
|
||||
<div class="info" id="info"></div>
|
||||
<div class="actions">
|
||||
<button id="btnDownload" disabled>下载 PNG</button>
|
||||
<button id="btnCopy" disabled>复制链接</button>
|
||||
<div id="errorBox"></div>
|
||||
<button class="btn-copy" id="btnCopy" disabled>复制 SVG</button>
|
||||
<button class="btn-png" id="btnPng" disabled>导出 PNG</button>
|
||||
<button class="btn-svg" id="btnSvg" disabled>导出 SVG</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
<div class="bottom" id="bottomInput"></div>
|
||||
|
||||
<script>
|
||||
const textEl = document.getElementById('text');
|
||||
const levelEl = document.getElementById('level');
|
||||
const marginEl = document.getElementById('margin');
|
||||
const sizeEl = document.getElementById('size');
|
||||
const preview = document.getElementById('preview');
|
||||
const info = document.getElementById('info');
|
||||
let timer, currentUrl = '';
|
||||
// ── 状态 ──
|
||||
let mode = 'text';
|
||||
let cfg = { level:'M', margin:4, size:8 };
|
||||
let currentPngUrl = '';
|
||||
|
||||
function update() {
|
||||
const text = textEl.value.trim();
|
||||
if (!text) { preview.innerHTML='<span style="color:#999">输入内容生成 QR 码</span>'; info.textContent=''; return; }
|
||||
// ── 每个模式下构建 QR 编码文本 ──
|
||||
function buildQrText() {
|
||||
switch (mode) {
|
||||
case 'url': return docVal('url') || '';
|
||||
case 'wifi': return (docVal('ssid') ? `WIFI:T:${docVal('enc','WPA')};S:${docVal('ssid')};P:${docVal('pwd')};${docVal('hidden')==='1'?'H:true;':''};` : '');
|
||||
case 'vcard': return `BEGIN:VCARD\nVERSION:3.0\nFN:${docVal('name')}\nTEL:${docVal('phone')}\nEMAIL:${docVal('email')}\nORG:${docVal('org')}\nADR:${docVal('addr')}\nEND:VCARD`;
|
||||
case 'email': return `mailto:${docVal('to')}?subject=${encodeURIComponent(docVal('sub'))}&body=${encodeURIComponent(docVal('body'))}`;
|
||||
case 'phone': return `tel:${docVal('num')}`;
|
||||
case 'sms': return `smsto:${docVal('snum')}:${docVal('msg')}`;
|
||||
default: return docVal('text') || '';
|
||||
}
|
||||
}
|
||||
function docVal(id,d){ const el=document.getElementById('f_'+id); return el ? (el.type==='checkbox' ? el.checked : el.value) : (d||''); }
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(async () => {
|
||||
const p = new URLSearchParams({ text, level: levelEl.value, margin: marginEl.value, size: sizeEl.value });
|
||||
currentUrl = `/api/qr?${p}`;
|
||||
preview.innerHTML = `<img src="${currentUrl}" alt="QR">`;
|
||||
document.getElementById('btnDownload').disabled = false;
|
||||
document.getElementById('btnCopy').disabled = false;
|
||||
info.textContent = `${p.get('size')}px/模块 · ${p.get('margin')}px边距`;
|
||||
}, 300);
|
||||
// ── 底部输入 — 模式表单 ──
|
||||
const MODE_FORMS = {
|
||||
text: '<textarea id="f_text" placeholder="输入任意文本…" class="flex:1" style="resize:none;height:100%"></textarea>',
|
||||
url: '<input id="f_url" type="url" placeholder="https://example.com" style="flex:1">',
|
||||
wifi: '<input id="f_ssid" placeholder="SSID" style="flex:1"><input id="f_pwd" type="password" placeholder="密码" style="flex:1"><select id="f_enc" style="flex:1"><option value="WPA">WPA/WPA2</option><option value="WEP">WEP</option><option value="nopass">无密码</option></select><label class="wifi-check"><input type="checkbox" id="f_hidden" value="1"> 隐藏</label>',
|
||||
vcard: '<input id="f_name" placeholder="姓名" style="flex:1"><input id="f_phone" placeholder="电话" style="flex:1"><input id="f_email" placeholder="邮箱" style="flex:1"><input id="f_org" placeholder="公司" style="flex:1"><input id="f_addr" placeholder="地址" style="flex:1">',
|
||||
email: '<input id="f_to" placeholder="收件人" style="flex:1"><input id="f_sub" placeholder="主题" style="flex:1"><input id="f_body" placeholder="正文" style="flex:2">',
|
||||
phone: '<input id="f_num" type="tel" placeholder="输入电话号码" style="flex:1">',
|
||||
sms: '<input id="f_snum" type="tel" placeholder="电话号码" style="flex:1"><input id="f_msg" placeholder="短信内容" style="flex:2">',
|
||||
};
|
||||
|
||||
function renderBottom() {
|
||||
document.getElementById('bottomInput').innerHTML = MODE_FORMS[mode] || MODE_FORMS.text;
|
||||
bindInputs();
|
||||
}
|
||||
|
||||
textEl.addEventListener('input', update);
|
||||
levelEl.addEventListener('change', update);
|
||||
marginEl.addEventListener('input', update);
|
||||
sizeEl.addEventListener('input', update);
|
||||
// ── 实时预览(防抖) ──
|
||||
let timer;
|
||||
function scheduleUpdate() {
|
||||
clearTimeout(timer);
|
||||
document.getElementById('previewLoading').style.display = 'block';
|
||||
document.getElementById('previewImg').style.display = 'none';
|
||||
document.getElementById('previewPlaceholder').style.display = 'none';
|
||||
timer = setTimeout(doUpdate, 200);
|
||||
}
|
||||
|
||||
document.getElementById('btnDownload').onclick = () => {
|
||||
if (currentUrl) { const a = document.createElement('a'); a.href = currentUrl; a.download = 'qrcode.png'; a.click(); }
|
||||
function doUpdate() {
|
||||
const text = buildQrText();
|
||||
if (!text) {
|
||||
document.getElementById('previewLoading').style.display = 'none';
|
||||
document.getElementById('previewPlaceholder').style.display = 'block';
|
||||
document.getElementById('previewInfo').textContent = '';
|
||||
document.getElementById('btnCopy').disabled = true;
|
||||
document.getElementById('btnPng').disabled = true;
|
||||
document.getElementById('btnSvg').disabled = true;
|
||||
currentPngUrl = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览用 PNG(size=8, 清晰不拉伸),下载用用户选择的 size
|
||||
const previewUrl = `/api/qr?text=${encodeURIComponent(text)}&level=${cfg.level}&margin=${cfg.margin}&size=8`;
|
||||
currentPngUrl = `/api/qr?text=${encodeURIComponent(text)}&level=${cfg.level}&margin=${cfg.margin}&size=${cfg.size}`;
|
||||
|
||||
const img = document.getElementById('previewImg');
|
||||
img.onload = () => {
|
||||
document.getElementById('previewLoading').style.display = 'none';
|
||||
img.style.display = 'block';
|
||||
};
|
||||
img.onerror = () => {
|
||||
document.getElementById('previewLoading').style.display = 'none';
|
||||
document.getElementById('previewPlaceholder').style.display = 'block';
|
||||
document.getElementById('previewPlaceholder').textContent = '编码失败';
|
||||
};
|
||||
img.src = previewUrl;
|
||||
document.getElementById('previewInfo').textContent = `${cfg.size}px/模块 · ${cfg.margin}px边距 · ${cfg.level}级`;
|
||||
document.getElementById('btnCopy').disabled = false;
|
||||
document.getElementById('btnPng').disabled = false;
|
||||
document.getElementById('btnSvg').disabled = false;
|
||||
}
|
||||
|
||||
// ── 事件绑定 ──
|
||||
function bindInputs() {
|
||||
document.querySelectorAll('#bottomInput input, #bottomInput textarea, #bottomInput select').forEach(el => {
|
||||
el.addEventListener('input', scheduleUpdate);
|
||||
});
|
||||
// URL 模式:失焦自动补 https://
|
||||
const urlEl = document.getElementById('f_url');
|
||||
if (urlEl) {
|
||||
urlEl.addEventListener('blur', () => {
|
||||
const v = urlEl.value.trim();
|
||||
if (v && !/^https?:\/\//i.test(v)) { urlEl.value = 'https://' + v; scheduleUpdate(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 模式切换 ──
|
||||
document.querySelectorAll('.sidebar-l button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sidebar-l button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
mode = btn.dataset.mode;
|
||||
renderBottom();
|
||||
scheduleUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 设置变更 ──
|
||||
['cfgLevel','cfgMargin','cfgSize'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el.addEventListener('input', () => {
|
||||
if (id === 'cfgLevel') cfg.level = el.value;
|
||||
if (id === 'cfgMargin') { cfg.margin = +el.value; document.getElementById('marginVal').textContent = el.value; }
|
||||
if (id === 'cfgSize') { cfg.size = +el.value; document.getElementById('sizeVal').textContent = el.value; }
|
||||
scheduleUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 导出按钮 ──
|
||||
document.getElementById('btnPng').onclick = () => {
|
||||
if (currentPngUrl) { const a = document.createElement('a'); a.href = currentPngUrl; a.download = 'qrcode.png'; a.click(); }
|
||||
};
|
||||
document.getElementById('btnSvg').onclick = () => {
|
||||
const text = buildQrText();
|
||||
if (!text) return;
|
||||
const url = `/api/qr?text=${encodeURIComponent(text)}&level=${cfg.level}&margin=${cfg.margin}&size=1&fmt=svg`;
|
||||
const a = document.createElement('a'); a.href = url; a.download = 'qrcode.svg'; a.click();
|
||||
};
|
||||
document.getElementById('btnCopy').onclick = async () => {
|
||||
if (currentUrl) {
|
||||
try { await navigator.clipboard.writeText(location.origin + currentUrl); alert('已复制'); }
|
||||
catch { prompt('复制以下链接:', location.origin + currentUrl); }
|
||||
try {
|
||||
const resp = await fetch(`/api/qr?text=${encodeURIComponent(buildQrText())}&level=${cfg.level}&margin=${cfg.margin}&size=1&fmt=svg`);
|
||||
const svg = await resp.text();
|
||||
await navigator.clipboard.writeText(svg);
|
||||
} catch(e) {
|
||||
const err = document.getElementById('errorBox');
|
||||
err.innerHTML = `<div class="error">复制失败</div>`;
|
||||
setTimeout(()=>err.innerHTML='',2000);
|
||||
}
|
||||
};
|
||||
|
||||
// ── 启动 ──
|
||||
renderBottom();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user