源码在最后
256
0%
15%
源码
<!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
<style>
.qr-card {
max-width: 720px;
margin: 1.5rem auto;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
}
.qr-row {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
margin: .5rem 0
}
.qr-row label {
font-size: .9rem;
color: #374151
}
.qr-row input[type="text"] {
flex: 1 1 320px;
padding: .6rem .8rem;
border: 1px solid #d1d5db;
border-radius: 10px
}
.qr-row input[type="number"] {
width: 5.5rem;
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px
}
.qr-row select,
.qr-row input[type="color"] {
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff
}
.qr-actions {
display: flex;
gap: .6rem;
flex-wrap: wrap;
margin-top: .75rem
}
.qr-btn {
appearance: none;
border: 1px solid #111827;
background: #111827;
color: #fff;
padding: .55rem .9rem;
border-radius: 999px;
cursor: pointer
}
.qr-btn.alt {
border-color: #d1d5db;
background: #fff;
color: #111827
}
.qr-preview {
display: grid;
place-items: center;
background: #f9fafb;
border-radius: 12px;
padding: 1rem;
margin-top: .75rem
}
.qr-size-output {
min-width: 3ch;
text-align: right;
font-variant-numeric: tabular-nums
}
.qr-row input[type="checkbox"] {
width: auto;
margin: 0 0.5rem;
}
.qr-row input[type="file"] {
font-size: 0.8rem;
padding: 0.3rem;
}
/* 二维码艺术效果样式 */
.qr-shadow {
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
}
.qr-glow {
filter: drop-shadow(0 0 10px currentColor);
}
.qr-3d {
filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
}
.qr-neon {
filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
}
/* 动画效果 */
.qr-pulse {
animation: qr-pulse 2s ease-in-out infinite;
}
.qr-rotate {
animation: qr-rotate 4s linear infinite;
}
.qr-fade {
animation: qr-fade 3s ease-in-out infinite;
}
@keyframes qr-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
@keyframes qr-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes qr-fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* 链接测试模态框 */
.qr-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.qr-modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: none;
border-radius: 12px;
width: 300px;
text-align: center;
}
.qr-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
.qr-card {
background: #111827;
border-color: #374151;
color: #e5e7eb
}
.qr-row input,
.qr-row select {
background: #0b1220;
color: #e5e7eb;
border-color: #374151
}
.qr-preview {
background: #0b1220
}
.qr-btn.alt {
background: transparent;
color: #e5e7eb;
border-color: #4b5563
}
.qr-modal-content {
background-color: #1f2937;
color: #e5e7eb;
}
.qr-modal-close {
color: #9ca3af;
}
}
</style>
<div class="qr-row">
<label>内容:</label>
<input id="qr-text" type="text" placeholder="要编码的文本/链接" />
<button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
</div>
<div class="qr-row">
<label>纠错:</label>
<select id="qr-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>
<label>尺寸(px):</label>
<input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
<span class="qr-size-output" id="qr-size-out">256</span>
<label>边距(模块):</label>
<input id="qr-margin" type="number" min="0" max="8" value="4" />
</div>
<div class="qr-row">
<label>样式模式:</label>
<select id="qr-style-mode">
<option value="classic">经典方块</option>
<option value="rounded">圆角方块</option>
<option value="dots">圆点</option>
<option value="artistic">艺术风格</option>
</select>
<label>圆角程度:</label>
<input id="qr-border-radius" type="range" min="0" max="50" value="0" />
<span class="qr-size-output" id="qr-radius-out">0%</span>
</div>
<div class="qr-row">
<label>颜色模式:</label>
<select id="qr-color-mode">
<option value="solid">单色</option>
<option value="gradient">线性渐变</option>
<option value="radial">径向渐变</option>
<option value="rainbow">彩虹渐变</option>
</select>
<label>渐变方向:</label>
<select id="qr-gradient-direction">
<option value="0">水平→</option>
<option value="90">垂直↓</option>
<option value="45">对角↘</option>
<option value="135">对角↙</option>
</select>
</div>
<div class="qr-row">
<label>前景色:</label>
<input id="qr-dark" type="color" value="#000000" />
<label>渐变终点色:</label>
<input id="qr-dark2" type="color" value="#4f46e5" />
<button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
</div>
<div class="qr-row">
<label>背景色:</label>
<input id="qr-light" type="color" value="#ffffff" />
<label>背景渐变色:</label>
<input id="qr-light2" type="color" value="#f3f4f6" />
<button class="qr-btn alt" id="qr-invert" type="button">反色</button>
</div>
<div class="qr-row">
<label>Logo图片:</label>
<input id="qr-logo" type="file" accept="image/*" />
<label>Logo大小:</label>
<input id="qr-logo-size" type="range" min="10" max="30" value="15" />
<span class="qr-size-output" id="qr-logo-size-out">15%</span>
<button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
</div>
<div class="qr-row">
<label>艺术效果:</label>
<select id="qr-art-effect">
<option value="none">无</option>
<option value="shadow">阴影</option>
<option value="glow">发光</option>
<option value="3d">3D效果</option>
<option value="neon">霓虹</option>
</select>
<label>动画效果:</label>
<select id="qr-animation">
<option value="none">无动画</option>
<option value="pulse">脉冲</option>
<option value="rotate">旋转</option>
<option value="fade">淡入淡出</option>
</select>
</div>
<div class="qr-row">
<label>链接自动跳转:</label>
<input id="qr-auto-redirect" type="checkbox" />
<label>新窗口打开:</label>
<input id="qr-new-window" type="checkbox" checked />
<button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
</div>
<div class="qr-actions">
<button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
<button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
<button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
</div>
<div class="qr-preview">
<div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
</div>
<canvas id="qr-canvas" style="display:none"></canvas>
<!-- 链接测试模态框 -->
<div id="qr-link-modal" class="qr-modal">
<div class="qr-modal-content">
<span class="qr-modal-close">×</span>
<h3>链接测试</h3>
<p id="qr-link-preview"></p>
<div style="margin-top: 15px;">
<button class="qr-btn" id="qr-open-link">打开链接</button>
<button class="qr-btn alt" id="qr-copy-link">复制链接</button>
</div>
</div>
</div>
</div>
<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>
<script>
// 等待库加载完成
window.addEventListener('load', () => {
const $ = (sel) => document.querySelector(sel);
const textEl = $('#qr-text');
const levelEl = $('#qr-level');
const sizeEl = $('#qr-size');
const sizeOut = $('#qr-size-out');
const marginEl = $('#qr-margin');
const darkEl = $('#qr-dark');
const dark2El = $('#qr-dark2');
const lightEl = $('#qr-light');
const light2El = $('#qr-light2');
const invertEl = $('#qr-invert');
const svgWrap = $('#qr-svg-wrap');
const canvas = $('#qr-canvas');
// 新增的控制元素
const styleModeEl = $('#qr-style-mode');
const borderRadiusEl = $('#qr-border-radius');
const radiusOut = $('#qr-radius-out');
const colorModeEl = $('#qr-color-mode');
const gradientDirectionEl = $('#qr-gradient-direction');
const logoEl = $('#qr-logo');
const logoSizeEl = $('#qr-logo-size');
const logoSizeOut = $('#qr-logo-size-out');
const artEffectEl = $('#qr-art-effect');
const animationEl = $('#qr-animation');
const autoRedirectEl = $('#qr-auto-redirect');
const newWindowEl = $('#qr-new-window');
// 模态框元素
const linkModal = $('#qr-link-modal');
const linkPreview = $('#qr-link-preview');
const modalClose = $('.qr-modal-close');
let logoImage = null;
// Canvas圆角矩形兼容性
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
this.beginPath();
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.quadraticCurveTo(x + width, y, x + width, y + radius);
this.lineTo(x + width, y + height - radius);
this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
this.lineTo(x + radius, y + height);
this.quadraticCurveTo(x, y + height, x, y + height - radius);
this.lineTo(x, y + radius);
this.quadraticCurveTo(x, y, x + radius, y);
this.closePath();
};
}
// 初始值:默认填入当前页 URL
if (!textEl.value) textEl.value = location.href;
const debounce = (fn, wait = 160) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};
function makeQRMatrix(content, level) {
const qr = qrcode(0, level); // 0 = 自动尺寸
qr.addData(content);
qr.make();
return qr;
}
// 颜色工具函数
function getRandomColor() {
return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
}
function createGradient(color1, color2, direction, type = 'linear') {
if (type === 'radial') {
return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
} else if (type === 'rainbow') {
return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
}
return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
}
function renderSVG(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid',
gradientDirection = 0,
artEffect = 'none',
animation = 'none'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const radius = borderRadius / 100 * 0.4; // 转换为相对单位
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
// 添加渐变定义
if (colorMode !== 'solid') {
svg += '<defs>';
// 前景渐变
if (colorMode === 'gradient') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</linearGradient>';
} else if (colorMode === 'radial') {
svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</radialGradient>';
} else if (colorMode === 'rainbow') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
});
svg += '</linearGradient>';
}
// 背景渐变
svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
svg += '</linearGradient>';
svg += '</defs>';
}
// 背景
const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
// 前景色
const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = c + border;
const y = r + border;
if (styleMode === 'dots') {
svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
} else if (styleMode === 'rounded' || radius > 0) {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
} else if (styleMode === 'artistic') {
// 艺术风格:随机大小和透明度
const size = 0.8 + Math.random() * 0.4;
const opacity = 0.7 + Math.random() * 0.3;
const offset = (1 - size) / 2;
svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
} else {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
}
}
}
}
svg += `</svg>`;
return svg;
}
function renderCanvas(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const scale = Math.max(1, Math.floor(size / units));
canvas.width = units * scale;
canvas.height = units * scale;
const ctx = canvas.getContext('2d');
// 背景渐变
if (colorMode !== 'solid') {
const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
bgGradient.addColorStop(0, light);
bgGradient.addColorStop(1, light2);
ctx.fillStyle = bgGradient;
} else {
ctx.fillStyle = light;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 前景渐变
let fgStyle = dark;
if (colorMode === 'gradient') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'radial') {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY);
const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'rainbow') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
fgGradient.addColorStop(i / (colors.length - 1), color);
});
fgStyle = fgGradient;
}
ctx.fillStyle = fgStyle;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = (c + border) * scale;
const y = (r + border) * scale;
if (styleMode === 'dots') {
ctx.beginPath();
ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
ctx.fill();
} else if (styleMode === 'rounded' || borderRadius > 0) {
const radius = (borderRadius / 100) * scale * 0.4;
ctx.beginPath();
ctx.roundRect(x, y, scale, scale, radius);
ctx.fill();
} else {
ctx.fillRect(x, y, scale, scale);
}
}
}
}
// 添加Logo
if (logoImage) {
const logoSize = (logoSizeEl.value / 100) * size;
const logoX = (canvas.width - logoSize) / 2;
const logoY = (canvas.height - logoSize) / 2;
// 白色背景圆形
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
ctx.fill();
// 绘制Logo
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
}
}
// 辅助函数
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function filenameBase() {
const raw = (textEl.value || '').trim();
try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
}
// Logo处理
function handleLogoUpload(event) {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
logoImage = img;
generate();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
}
// 随机配色
function randomColors() {
darkEl.value = getRandomColor();
dark2El.value = getRandomColor();
lightEl.value = getRandomColor();
light2El.value = getRandomColor();
generate();
}
// 清除Logo
function clearLogo() {
logoImage = null;
logoEl.value = '';
generate();
}
// 链接测试
function testLink() {
const content = textEl.value.trim();
if (!content) {
alert('请先输入内容');
return;
}
linkPreview.textContent = content;
linkModal.style.display = 'block';
}
// 打开链接
function openLink() {
const content = textEl.value.trim();
if (isValidUrl(content)) {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
} else {
alert('不是有效的URL链接');
}
linkModal.style.display = 'none';
}
// 复制链接
async function copyLink() {
const content = textEl.value.trim();
try {
await navigator.clipboard.writeText(content);
alert('链接已复制到剪贴板');
} catch {
const t = document.createElement('textarea');
t.value = content;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
document.body.removeChild(t);
alert('链接已复制到剪贴板');
}
linkModal.style.display = 'none';
}
const generate = () => {
const content = (textEl.value || '').trim();
if (!content) {
svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>';
return;
}
const level = levelEl.value || 'M';
const size = parseInt(sizeEl.value, 10) || 256;
const margin = parseInt(marginEl.value, 10) || 4;
const options = {
dark: darkEl.value || '#000000',
dark2: dark2El.value || '#4f46e5',
light: lightEl.value || '#ffffff',
light2: light2El.value || '#f3f4f6',
styleMode: styleModeEl.value || 'classic',
borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
colorMode: colorModeEl.value || 'solid',
gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
artEffect: artEffectEl.value || 'none',
animation: animationEl.value || 'none'
};
const qr = makeQRMatrix(content, level);
const svg = renderSVG(qr, size, margin, options);
// 应用艺术效果和动画
let finalSvg = svg;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = svg;
const svgElement = tempDiv.querySelector('svg');
if (options.artEffect !== 'none') {
svgElement.classList.add(`qr-${options.artEffect}`);
}
if (options.animation !== 'none') {
svgElement.classList.add(`qr-${options.animation}`);
}
svgWrap.innerHTML = tempDiv.innerHTML;
renderCanvas(qr, size, margin, options);
sizeOut.textContent = size;
radiusOut.textContent = options.borderRadius + '%';
logoSizeOut.textContent = logoSizeEl.value + '%';
// 自动跳转功能
if (autoRedirectEl.checked && isValidUrl(content)) {
svgWrap.style.cursor = 'pointer';
svgWrap.title = '点击访问链接';
svgWrap.onclick = () => {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
};
} else {
svgWrap.style.cursor = 'default';
svgWrap.title = '';
svgWrap.onclick = null;
}
};
// 下载 PNG
const downloadPNG = () => {
const a = document.createElement('a');
a.download = `${filenameBase()}.png`;
a.href = canvas.toDataURL('image/png');
a.click();
};
// 下载 SVG
const downloadSVG = () => {
const svgEl = svgWrap.querySelector('svg');
if (!svgEl) return;
const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = `${filenameBase()}.svg`;
a.href = url;
a.click();
URL.revokeObjectURL(url);
};
// 复制 DataURL(PNG)
const copyDataURL = async () => {
const data = canvas.toDataURL('image/png');
try {
await navigator.clipboard.writeText(data);
alert('已复制 PNG DataURL 到剪贴板');
} catch {
// 回退:创建临时输入框
const t = document.createElement('textarea');
t.value = data; document.body.appendChild(t); t.select();
document.execCommand('copy'); document.body.removeChild(t);
alert('已复制 PNG DataURL 到剪贴板');
}
};
// 反色
const invert = () => {
const oldDark = darkEl.value, oldLight = lightEl.value;
darkEl.value = oldLight; lightEl.value = oldDark; generate();
};
// 事件绑定(带轻微防抖,输入体验更好)
textEl.addEventListener('input', debounce(generate, 180));
// 所有控制元素的变化事件
[levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El,
styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl,
logoSizeEl, artEffectEl, animationEl].forEach(el =>
el.addEventListener('input', generate)
);
// 按钮事件
$('#qr-download-png').addEventListener('click', downloadPNG);
$('#qr-download-svg').addEventListener('click', downloadSVG);
$('#qr-copy-dataurl').addEventListener('click', copyDataURL);
$('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
invertEl.addEventListener('click', invert);
$('#qr-random-colors').addEventListener('click', randomColors);
$('#qr-clear-logo').addEventListener('click', clearLogo);
$('#qr-test-link').addEventListener('click', testLink);
// Logo上传
logoEl.addEventListener('change', handleLogoUpload);
// 模态框事件
modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
$('#qr-open-link').addEventListener('click', openLink);
$('#qr-copy-link').addEventListener('click', copyLink);
// 点击模态框外部关闭
window.addEventListener('click', (event) => {
if (event.target === linkModal) {
linkModal.style.display = 'none';
}
});
// 复选框事件
autoRedirectEl.addEventListener('change', generate);
newWindowEl.addEventListener('change', generate);
// 首次渲染
generate();
});
</script><!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
<style>
.qr-card {
max-width: 720px;
margin: 1.5rem auto;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
}
.qr-row {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
margin: .5rem 0
}
.qr-row label {
font-size: .9rem;
color: #374151
}
.qr-row input[type="text"] {
flex: 1 1 320px;
padding: .6rem .8rem;
border: 1px solid #d1d5db;
border-radius: 10px
}
.qr-row input[type="number"] {
width: 5.5rem;
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px
}
.qr-row select,
.qr-row input[type="color"] {
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff
}
.qr-actions {
display: flex;
gap: .6rem;
flex-wrap: wrap;
margin-top: .75rem
}
.qr-btn {
appearance: none;
border: 1px solid #111827;
background: #111827;
color: #fff;
padding: .55rem .9rem;
border-radius: 999px;
cursor: pointer
}
.qr-btn.alt {
border-color: #d1d5db;
background: #fff;
color: #111827
}
.qr-preview {
display: grid;
place-items: center;
background: #f9fafb;
border-radius: 12px;
padding: 1rem;
margin-top: .75rem
}
.qr-size-output {
min-width: 3ch;
text-align: right;
font-variant-numeric: tabular-nums
}
.qr-row input[type="checkbox"] {
width: auto;
margin: 0 0.5rem;
}
.qr-row input[type="file"] {
font-size: 0.8rem;
padding: 0.3rem;
}
/* 二维码艺术效果样式 */
.qr-shadow {
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
}
.qr-glow {
filter: drop-shadow(0 0 10px currentColor);
}
.qr-3d {
filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
}
.qr-neon {
filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
}
/* 动画效果 */
.qr-pulse {
animation: qr-pulse 2s ease-in-out infinite;
}
.qr-rotate {
animation: qr-rotate 4s linear infinite;
}
.qr-fade {
animation: qr-fade 3s ease-in-out infinite;
}
@keyframes qr-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
@keyframes qr-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes qr-fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* 链接测试模态框 */
.qr-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.qr-modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: none;
border-radius: 12px;
width: 300px;
text-align: center;
}
.qr-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
.qr-card {
background: #111827;
border-color: #374151;
color: #e5e7eb
}
.qr-row input,
.qr-row select {
background: #0b1220;
color: #e5e7eb;
border-color: #374151
}
.qr-preview {
background: #0b1220
}
.qr-btn.alt {
background: transparent;
color: #e5e7eb;
border-color: #4b5563
}
.qr-modal-content {
background-color: #1f2937;
color: #e5e7eb;
}
.qr-modal-close {
color: #9ca3af;
}
}
</style>
<div class="qr-row">
<label>内容:</label>
<input id="qr-text" type="text" placeholder="要编码的文本/链接" />
<button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
</div>
<div class="qr-row">
<label>纠错:</label>
<select id="qr-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>
<label>尺寸(px):</label>
<input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
<span class="qr-size-output" id="qr-size-out">256</span>
<label>边距(模块):</label>
<input id="qr-margin" type="number" min="0" max="8" value="4" />
</div>
<div class="qr-row">
<label>样式模式:</label>
<select id="qr-style-mode">
<option value="classic">经典方块</option>
<option value="rounded">圆角方块</option>
<option value="dots">圆点</option>
<option value="artistic">艺术风格</option>
</select>
<label>圆角程度:</label>
<input id="qr-border-radius" type="range" min="0" max="50" value="0" />
<span class="qr-size-output" id="qr-radius-out">0%</span>
</div>
<div class="qr-row">
<label>颜色模式:</label>
<select id="qr-color-mode">
<option value="solid">单色</option>
<option value="gradient">线性渐变</option>
<option value="radial">径向渐变</option>
<option value="rainbow">彩虹渐变</option>
</select>
<label>渐变方向:</label>
<select id="qr-gradient-direction">
<option value="0">水平→</option>
<option value="90">垂直↓</option>
<option value="45">对角↘</option>
<option value="135">对角↙</option>
</select>
</div>
<div class="qr-row">
<label>前景色:</label>
<input id="qr-dark" type="color" value="#000000" />
<label>渐变终点色:</label>
<input id="qr-dark2" type="color" value="#4f46e5" />
<button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
</div>
<div class="qr-row">
<label>背景色:</label>
<input id="qr-light" type="color" value="#ffffff" />
<label>背景渐变色:</label>
<input id="qr-light2" type="color" value="#f3f4f6" />
<button class="qr-btn alt" id="qr-invert" type="button">反色</button>
</div>
<div class="qr-row">
<label>Logo图片:</label>
<input id="qr-logo" type="file" accept="image/*" />
<label>Logo大小:</label>
<input id="qr-logo-size" type="range" min="10" max="30" value="15" />
<span class="qr-size-output" id="qr-logo-size-out">15%</span>
<button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
</div>
<div class="qr-row">
<label>艺术效果:</label>
<select id="qr-art-effect">
<option value="none">无</option>
<option value="shadow">阴影</option>
<option value="glow">发光</option>
<option value="3d">3D效果</option>
<option value="neon">霓虹</option>
</select>
<label>动画效果:</label>
<select id="qr-animation">
<option value="none">无动画</option>
<option value="pulse">脉冲</option>
<option value="rotate">旋转</option>
<option value="fade">淡入淡出</option>
</select>
</div>
<div class="qr-row">
<label>链接自动跳转:</label>
<input id="qr-auto-redirect" type="checkbox" />
<label>新窗口打开:</label>
<input id="qr-new-window" type="checkbox" checked />
<button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
</div>
<div class="qr-actions">
<button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
<button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
<button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
</div>
<div class="qr-preview">
<div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
</div>
<canvas id="qr-canvas" style="display:none"></canvas>
<!-- 链接测试模态框 -->
<div id="qr-link-modal" class="qr-modal">
<div class="qr-modal-content">
<span class="qr-modal-close">×</span>
<h3>链接测试</h3>
<p id="qr-link-preview"></p>
<div style="margin-top: 15px;">
<button class="qr-btn" id="qr-open-link">打开链接</button>
<button class="qr-btn alt" id="qr-copy-link">复制链接</button>
</div>
</div>
</div>
</div>
<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>
<script>
// 等待库加载完成
window.addEventListener('load', () => {
const $ = (sel) => document.querySelector(sel);
const textEl = $('#qr-text');
const levelEl = $('#qr-level');
const sizeEl = $('#qr-size');
const sizeOut = $('#qr-size-out');
const marginEl = $('#qr-margin');
const darkEl = $('#qr-dark');
const dark2El = $('#qr-dark2');
const lightEl = $('#qr-light');
const light2El = $('#qr-light2');
const invertEl = $('#qr-invert');
const svgWrap = $('#qr-svg-wrap');
const canvas = $('#qr-canvas');
// 新增的控制元素
const styleModeEl = $('#qr-style-mode');
const borderRadiusEl = $('#qr-border-radius');
const radiusOut = $('#qr-radius-out');
const colorModeEl = $('#qr-color-mode');
const gradientDirectionEl = $('#qr-gradient-direction');
const logoEl = $('#qr-logo');
const logoSizeEl = $('#qr-logo-size');
const logoSizeOut = $('#qr-logo-size-out');
const artEffectEl = $('#qr-art-effect');
const animationEl = $('#qr-animation');
const autoRedirectEl = $('#qr-auto-redirect');
const newWindowEl = $('#qr-new-window');
// 模态框元素
const linkModal = $('#qr-link-modal');
const linkPreview = $('#qr-link-preview');
const modalClose = $('.qr-modal-close');
let logoImage = null;
// Canvas圆角矩形兼容性
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
this.beginPath();
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.quadraticCurveTo(x + width, y, x + width, y + radius);
this.lineTo(x + width, y + height - radius);
this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
this.lineTo(x + radius, y + height);
this.quadraticCurveTo(x, y + height, x, y + height - radius);
this.lineTo(x, y + radius);
this.quadraticCurveTo(x, y, x + radius, y);
this.closePath();
};
}
// 初始值:默认填入当前页 URL
if (!textEl.value) textEl.value = location.href;
const debounce = (fn, wait = 160) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};
function makeQRMatrix(content, level) {
const qr = qrcode(0, level); // 0 = 自动尺寸
qr.addData(content);
qr.make();
return qr;
}
// 颜色工具函数
function getRandomColor() {
return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
}
function createGradient(color1, color2, direction, type = 'linear') {
if (type === 'radial') {
return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
} else if (type === 'rainbow') {
return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
}
return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
}
function renderSVG(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid',
gradientDirection = 0,
artEffect = 'none',
animation = 'none'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const radius = borderRadius / 100 * 0.4; // 转换为相对单位
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
// 添加渐变定义
if (colorMode !== 'solid') {
svg += '<defs>';
// 前景渐变
if (colorMode === 'gradient') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</linearGradient>';
} else if (colorMode === 'radial') {
svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</radialGradient>';
} else if (colorMode === 'rainbow') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
});
svg += '</linearGradient>';
}
// 背景渐变
svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
svg += '</linearGradient>';
svg += '</defs>';
}
// 背景
const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
// 前景色
const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = c + border;
const y = r + border;
if (styleMode === 'dots') {
svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
} else if (styleMode === 'rounded' || radius > 0) {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
} else if (styleMode === 'artistic') {
// 艺术风格:随机大小和透明度
const size = 0.8 + Math.random() * 0.4;
const opacity = 0.7 + Math.random() * 0.3;
const offset = (1 - size) / 2;
svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
} else {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
}
}
}
}
svg += `</svg>`;
return svg;
}
function renderCanvas(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const scale = Math.max(1, Math.floor(size / units));
canvas.width = units * scale;
canvas.height = units * scale;
const ctx = canvas.getContext('2d');
// 背景渐变
if (colorMode !== 'solid') {
const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
bgGradient.addColorStop(0, light);
bgGradient.addColorStop(1, light2);
ctx.fillStyle = bgGradient;
} else {
ctx.fillStyle = light;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 前景渐变
let fgStyle = dark;
if (colorMode === 'gradient') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'radial') {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY);
const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'rainbow') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
fgGradient.addColorStop(i / (colors.length - 1), color);
});
fgStyle = fgGradient;
}
ctx.fillStyle = fgStyle;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = (c + border) * scale;
const y = (r + border) * scale;
if (styleMode === 'dots') {
ctx.beginPath();
ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
ctx.fill();
} else if (styleMode === 'rounded' || borderRadius > 0) {
const radius = (borderRadius / 100) * scale * 0.4;
ctx.beginPath();
ctx.roundRect(x, y, scale, scale, radius);
ctx.fill();
} else {
ctx.fillRect(x, y, scale, scale);
}
}
}
}
// 添加Logo
if (logoImage) {
const logoSize = (logoSizeEl.value / 100) * size;
const logoX = (canvas.width - logoSize) / 2;
const logoY = (canvas.height - logoSize) / 2;
// 白色背景圆形
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
ctx.fill();
// 绘制Logo
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
}
}
// 辅助函数
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function filenameBase() {
const raw = (textEl.value || '').trim();
try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
}
// Logo处理
function handleLogoUpload(event) {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
logoImage = img;
generate();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
}
// 随机配色
function randomColors() {
darkEl.value = getRandomColor();
dark2El.value = getRandomColor();
lightEl.value = getRandomColor();
light2El.value = getRandomColor();
generate();
}
// 清除Logo
function clearLogo() {
logoImage = null;
logoEl.value = '';
generate();
}
// 链接测试
function testLink() {
const content = textEl.value.trim();
if (!content) {
alert('请先输入内容');
return;
}
linkPreview.textContent = content;
linkModal.style.display = 'block';
}
// 打开链接
function openLink() {
const content = textEl.value.trim();
if (isValidUrl(content)) {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
} else {
alert('不是有效的URL链接');
}
linkModal.style.display = 'none';
}
// 复制链接
async function copyLink() {
const content = textEl.value.trim();
try {
await navigator.clipboard.writeText(content);
alert('链接已复制到剪贴板');
} catch {
const t = document.createElement('textarea');
t.value = content;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
document.body.removeChild(t);
alert('链接已复制到剪贴板');
}
linkModal.style.display = 'none';
}
const generate = () => {
const content = (textEl.value || '').trim();
if (!content) {
svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>';
return;
}
const level = levelEl.value || 'M';
const size = parseInt(sizeEl.value, 10) || 256;
const margin = parseInt(marginEl.value, 10) || 4;
const options = {
dark: darkEl.value || '#000000',
dark2: dark2El.value || '#4f46e5',
light: lightEl.value || '#ffffff',
light2: light2El.value || '#f3f4f6',
styleMode: styleModeEl.value || 'classic',
borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
colorMode: colorModeEl.value || 'solid',
gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
artEffect: artEffectEl.value || 'none',
animation: animationEl.value || 'none'
};
const qr = makeQRMatrix(content, level);
const svg = renderSVG(qr, size, margin, options);
// 应用艺术效果和动画
let finalSvg = svg;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = svg;
const svgElement = tempDiv.querySelector('svg');
if (options.artEffect !== 'none') {
svgElement.classList.add(`qr-${options.artEffect}`);
}
if (options.animation !== 'none') {
svgElement.classList.add(`qr-${options.animation}`);
}
svgWrap.innerHTML = tempDiv.innerHTML;
renderCanvas(qr, size, margin, options);
sizeOut.textContent = size;
radiusOut.textContent = options.borderRadius + '%';
logoSizeOut.textContent = logoSizeEl.value + '%';
// 自动跳转功能
if (autoRedirectEl.checked && isValidUrl(content)) {
svgWrap.style.cursor = 'pointer';
svgWrap.title = '点击访问链接';
svgWrap.onclick = () => {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
};
} else {
svgWrap.style.cursor = 'default';
svgWrap.title = '';
svgWrap.onclick = null;
}
};
// 下载 PNG
const downloadPNG = () => {
const a = document.createElement('a');
a.download = `${filenameBase()}.png`;
a.href = canvas.toDataURL('image/png');
a.click();
};
// 下载 SVG
const downloadSVG = () => {
const svgEl = svgWrap.querySelector('svg');
if (!svgEl) return;
const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = `${filenameBase()}.svg`;
a.href = url;
a.click();
URL.revokeObjectURL(url);
};
// 复制 DataURL(PNG)
const copyDataURL = async () => {
const data = canvas.toDataURL('image/png');
try {
await navigator.clipboard.writeText(data);
alert('已复制 PNG DataURL 到剪贴板');
} catch {
// 回退:创建临时输入框
const t = document.createElement('textarea');
t.value = data; document.body.appendChild(t); t.select();
document.execCommand('copy'); document.body.removeChild(t);
alert('已复制 PNG DataURL 到剪贴板');
}
};
// 反色
const invert = () => {
const oldDark = darkEl.value, oldLight = lightEl.value;
darkEl.value = oldLight; lightEl.value = oldDark; generate();
};
// 事件绑定(带轻微防抖,输入体验更好)
textEl.addEventListener('input', debounce(generate, 180));
// 所有控制元素的变化事件
[levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El,
styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl,
logoSizeEl, artEffectEl, animationEl].forEach(el =>
el.addEventListener('input', generate)
);
// 按钮事件
$('#qr-download-png').addEventListener('click', downloadPNG);
$('#qr-download-svg').addEventListener('click', downloadSVG);
$('#qr-copy-dataurl').addEventListener('click', copyDataURL);
$('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
invertEl.addEventListener('click', invert);
$('#qr-random-colors').addEventListener('click', randomColors);
$('#qr-clear-logo').addEventListener('click', clearLogo);
$('#qr-test-link').addEventListener('click', testLink);
// Logo上传
logoEl.addEventListener('change', handleLogoUpload);
// 模态框事件
modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
$('#qr-open-link').addEventListener('click', openLink);
$('#qr-copy-link').addEventListener('click', copyLink);
// 点击模态框外部关闭
window.addEventListener('click', (event) => {
if (event.target === linkModal) {
linkModal.style.display = 'none';
}
});
// 复选框事件
autoRedirectEl.addEventListener('change', generate);
newWindowEl.addEventListener('change', generate);
// 首次渲染
generate();
});
</script><!-- ✅ 直接粘贴到你的博客页面(文章 HTML 模式)即可使用 -->
<div class="qr-card" id="qr-widget">
<style>
.qr-card {
max-width: 720px;
margin: 1.5rem auto;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, .05);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
}
.qr-row {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
margin: .5rem 0
}
.qr-row label {
font-size: .9rem;
color: #374151
}
.qr-row input[type="text"] {
flex: 1 1 320px;
padding: .6rem .8rem;
border: 1px solid #d1d5db;
border-radius: 10px
}
.qr-row input[type="number"] {
width: 5.5rem;
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px
}
.qr-row select,
.qr-row input[type="color"] {
padding: .4rem .5rem;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff
}
.qr-actions {
display: flex;
gap: .6rem;
flex-wrap: wrap;
margin-top: .75rem
}
.qr-btn {
appearance: none;
border: 1px solid #111827;
background: #111827;
color: #fff;
padding: .55rem .9rem;
border-radius: 999px;
cursor: pointer
}
.qr-btn.alt {
border-color: #d1d5db;
background: #fff;
color: #111827
}
.qr-preview {
display: grid;
place-items: center;
background: #f9fafb;
border-radius: 12px;
padding: 1rem;
margin-top: .75rem
}
.qr-size-output {
min-width: 3ch;
text-align: right;
font-variant-numeric: tabular-nums
}
.qr-row input[type="checkbox"] {
width: auto;
margin: 0 0.5rem;
}
.qr-row input[type="file"] {
font-size: 0.8rem;
padding: 0.3rem;
}
/* 二维码艺术效果样式 */
.qr-shadow {
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.3));
}
.qr-glow {
filter: drop-shadow(0 0 10px currentColor);
}
.qr-3d {
filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.3)) drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.2));
}
.qr-neon {
filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 15px currentColor) drop-shadow(0 0 25px currentColor);
}
/* 动画效果 */
.qr-pulse {
animation: qr-pulse 2s ease-in-out infinite;
}
.qr-rotate {
animation: qr-rotate 4s linear infinite;
}
.qr-fade {
animation: qr-fade 3s ease-in-out infinite;
}
@keyframes qr-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
@keyframes qr-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes qr-fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* 链接测试模态框 */
.qr-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.qr-modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: none;
border-radius: 12px;
width: 300px;
text-align: center;
}
.qr-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
.qr-card {
background: #111827;
border-color: #374151;
color: #e5e7eb
}
.qr-row input,
.qr-row select {
background: #0b1220;
color: #e5e7eb;
border-color: #374151
}
.qr-preview {
background: #0b1220
}
.qr-btn.alt {
background: transparent;
color: #e5e7eb;
border-color: #4b5563
}
.qr-modal-content {
background-color: #1f2937;
color: #e5e7eb;
}
.qr-modal-close {
color: #9ca3af;
}
}
</style>
<div class="qr-row">
<label>内容:</label>
<input id="qr-text" type="text" placeholder="要编码的文本/链接" />
<button class="qr-btn alt" id="qr-fill-url" type="button">填入当前页 URL</button>
</div>
<div class="qr-row">
<label>纠错:</label>
<select id="qr-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>
<label>尺寸(px):</label>
<input id="qr-size" type="range" min="128" max="1024" step="32" value="256" />
<span class="qr-size-output" id="qr-size-out">256</span>
<label>边距(模块):</label>
<input id="qr-margin" type="number" min="0" max="8" value="4" />
</div>
<div class="qr-row">
<label>样式模式:</label>
<select id="qr-style-mode">
<option value="classic">经典方块</option>
<option value="rounded">圆角方块</option>
<option value="dots">圆点</option>
<option value="artistic">艺术风格</option>
</select>
<label>圆角程度:</label>
<input id="qr-border-radius" type="range" min="0" max="50" value="0" />
<span class="qr-size-output" id="qr-radius-out">0%</span>
</div>
<div class="qr-row">
<label>颜色模式:</label>
<select id="qr-color-mode">
<option value="solid">单色</option>
<option value="gradient">线性渐变</option>
<option value="radial">径向渐变</option>
<option value="rainbow">彩虹渐变</option>
</select>
<label>渐变方向:</label>
<select id="qr-gradient-direction">
<option value="0">水平→</option>
<option value="90">垂直↓</option>
<option value="45">对角↘</option>
<option value="135">对角↙</option>
</select>
</div>
<div class="qr-row">
<label>前景色:</label>
<input id="qr-dark" type="color" value="#000000" />
<label>渐变终点色:</label>
<input id="qr-dark2" type="color" value="#4f46e5" />
<button class="qr-btn alt" id="qr-random-colors" type="button">随机配色</button>
</div>
<div class="qr-row">
<label>背景色:</label>
<input id="qr-light" type="color" value="#ffffff" />
<label>背景渐变色:</label>
<input id="qr-light2" type="color" value="#f3f4f6" />
<button class="qr-btn alt" id="qr-invert" type="button">反色</button>
</div>
<div class="qr-row">
<label>Logo图片:</label>
<input id="qr-logo" type="file" accept="image/*" />
<label>Logo大小:</label>
<input id="qr-logo-size" type="range" min="10" max="30" value="15" />
<span class="qr-size-output" id="qr-logo-size-out">15%</span>
<button class="qr-btn alt" id="qr-clear-logo" type="button">清除Logo</button>
</div>
<div class="qr-row">
<label>艺术效果:</label>
<select id="qr-art-effect">
<option value="none">无</option>
<option value="shadow">阴影</option>
<option value="glow">发光</option>
<option value="3d">3D效果</option>
<option value="neon">霓虹</option>
</select>
<label>动画效果:</label>
<select id="qr-animation">
<option value="none">无动画</option>
<option value="pulse">脉冲</option>
<option value="rotate">旋转</option>
<option value="fade">淡入淡出</option>
</select>
</div>
<div class="qr-row">
<label>链接自动跳转:</label>
<input id="qr-auto-redirect" type="checkbox" />
<label>新窗口打开:</label>
<input id="qr-new-window" type="checkbox" checked />
<button class="qr-btn alt" id="qr-test-link" type="button">测试链接</button>
</div>
<div class="qr-actions">
<button class="qr-btn" id="qr-download-png" type="button">下载 PNG</button>
<button class="qr-btn alt" id="qr-download-svg" type="button">下载 SVG</button>
<button class="qr-btn alt" id="qr-copy-dataurl" type="button">复制图片 DataURL</button>
</div>
<div class="qr-preview">
<div id="qr-svg-wrap" aria-label="QR 预览" role="img"></div>
</div>
<canvas id="qr-canvas" style="display:none"></canvas>
<!-- 链接测试模态框 -->
<div id="qr-link-modal" class="qr-modal">
<div class="qr-modal-content">
<span class="qr-modal-close">×</span>
<h3>链接测试</h3>
<p id="qr-link-preview"></p>
<div style="margin-top: 15px;">
<button class="qr-btn" id="qr-open-link">打开链接</button>
<button class="qr-btn alt" id="qr-copy-link">复制链接</button>
</div>
</div>
</div>
</div>
<!-- 轻量二维码库(浏览器端生成矩阵): -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js" defer></script>
<script>
// 等待库加载完成
window.addEventListener('load', () => {
const $ = (sel) => document.querySelector(sel);
const textEl = $('#qr-text');
const levelEl = $('#qr-level');
const sizeEl = $('#qr-size');
const sizeOut = $('#qr-size-out');
const marginEl = $('#qr-margin');
const darkEl = $('#qr-dark');
const dark2El = $('#qr-dark2');
const lightEl = $('#qr-light');
const light2El = $('#qr-light2');
const invertEl = $('#qr-invert');
const svgWrap = $('#qr-svg-wrap');
const canvas = $('#qr-canvas');
// 新增的控制元素
const styleModeEl = $('#qr-style-mode');
const borderRadiusEl = $('#qr-border-radius');
const radiusOut = $('#qr-radius-out');
const colorModeEl = $('#qr-color-mode');
const gradientDirectionEl = $('#qr-gradient-direction');
const logoEl = $('#qr-logo');
const logoSizeEl = $('#qr-logo-size');
const logoSizeOut = $('#qr-logo-size-out');
const artEffectEl = $('#qr-art-effect');
const animationEl = $('#qr-animation');
const autoRedirectEl = $('#qr-auto-redirect');
const newWindowEl = $('#qr-new-window');
// 模态框元素
const linkModal = $('#qr-link-modal');
const linkPreview = $('#qr-link-preview');
const modalClose = $('.qr-modal-close');
let logoImage = null;
// Canvas圆角矩形兼容性
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
this.beginPath();
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.quadraticCurveTo(x + width, y, x + width, y + radius);
this.lineTo(x + width, y + height - radius);
this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
this.lineTo(x + radius, y + height);
this.quadraticCurveTo(x, y + height, x, y + height - radius);
this.lineTo(x, y + radius);
this.quadraticCurveTo(x, y, x + radius, y);
this.closePath();
};
}
// 初始值:默认填入当前页 URL
if (!textEl.value) textEl.value = location.href;
const debounce = (fn, wait = 160) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};
function makeQRMatrix(content, level) {
const qr = qrcode(0, level); // 0 = 自动尺寸
qr.addData(content);
qr.make();
return qr;
}
// 颜色工具函数
function getRandomColor() {
return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
}
function createGradient(color1, color2, direction, type = 'linear') {
if (type === 'radial') {
return `radial-gradient(circle, ${color1} 0%, ${color2} 100%)`;
} else if (type === 'rainbow') {
return 'linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3)';
}
return `linear-gradient(${direction}deg, ${color1} 0%, ${color2} 100%)`;
}
function renderSVG(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid',
gradientDirection = 0,
artEffect = 'none',
animation = 'none'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const radius = borderRadius / 100 * 0.4; // 转换为相对单位
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${units} ${units}" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true">`;
// 添加渐变定义
if (colorMode !== 'solid') {
svg += '<defs>';
// 前景渐变
if (colorMode === 'gradient') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="${gradientDirection == 0 ? '100%' : '0%'}" y2="${gradientDirection == 90 ? '100%' : gradientDirection == 45 ? '100%' : '0%'}">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</linearGradient>';
} else if (colorMode === 'radial') {
svg += `<radialGradient id="fg-gradient" cx="50%" cy="50%" r="50%">`;
svg += `<stop offset="0%" style="stop-color:${dark};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${dark2};stop-opacity:1" />`;
svg += '</radialGradient>';
} else if (colorMode === 'rainbow') {
svg += `<linearGradient id="fg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
svg += `<stop offset="${(i * 100 / (colors.length - 1))}%" style="stop-color:${color};stop-opacity:1" />`;
});
svg += '</linearGradient>';
}
// 背景渐变
svg += `<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">`;
svg += `<stop offset="0%" style="stop-color:${light};stop-opacity:1" />`;
svg += `<stop offset="100%" style="stop-color:${light2};stop-opacity:1" />`;
svg += '</linearGradient>';
svg += '</defs>';
}
// 背景
const bgFill = colorMode !== 'solid' ? 'url(#bg-gradient)' : light;
svg += `<rect fill="${bgFill}" x="0" y="0" width="${units}" height="${units}"/>`;
// 前景色
const fgFill = colorMode !== 'solid' ? 'url(#fg-gradient)' : dark;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = c + border;
const y = r + border;
if (styleMode === 'dots') {
svg += `<circle fill="${fgFill}" cx="${x + 0.5}" cy="${y + 0.5}" r="0.4"/>`;
} else if (styleMode === 'rounded' || radius > 0) {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1" rx="${radius}" ry="${radius}"/>`;
} else if (styleMode === 'artistic') {
// 艺术风格:随机大小和透明度
const size = 0.8 + Math.random() * 0.4;
const opacity = 0.7 + Math.random() * 0.3;
const offset = (1 - size) / 2;
svg += `<rect fill="${fgFill}" fill-opacity="${opacity}" x="${x + offset}" y="${y + offset}" width="${size}" height="${size}" rx="${radius}"/>`;
} else {
svg += `<rect fill="${fgFill}" x="${x}" y="${y}" width="1" height="1"/>`;
}
}
}
}
svg += `</svg>`;
return svg;
}
function renderCanvas(qr, size, margin, options = {}) {
const {
dark = '#000000',
dark2 = '#4f46e5',
light = '#ffffff',
light2 = '#f3f4f6',
styleMode = 'classic',
borderRadius = 0,
colorMode = 'solid'
} = options;
const n = qr.getModuleCount();
const border = Math.max(0, margin | 0);
const units = n + 2 * border;
const scale = Math.max(1, Math.floor(size / units));
canvas.width = units * scale;
canvas.height = units * scale;
const ctx = canvas.getContext('2d');
// 背景渐变
if (colorMode !== 'solid') {
const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
bgGradient.addColorStop(0, light);
bgGradient.addColorStop(1, light2);
ctx.fillStyle = bgGradient;
} else {
ctx.fillStyle = light;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 前景渐变
let fgStyle = dark;
if (colorMode === 'gradient') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'radial') {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY);
const fgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
fgGradient.addColorStop(0, dark);
fgGradient.addColorStop(1, dark2);
fgStyle = fgGradient;
} else if (colorMode === 'rainbow') {
const fgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
colors.forEach((color, i) => {
fgGradient.addColorStop(i / (colors.length - 1), color);
});
fgStyle = fgGradient;
}
ctx.fillStyle = fgStyle;
// 渲染模块
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
if (qr.isDark(r, c)) {
const x = (c + border) * scale;
const y = (r + border) * scale;
if (styleMode === 'dots') {
ctx.beginPath();
ctx.arc(x + scale/2, y + scale/2, scale * 0.4, 0, 2 * Math.PI);
ctx.fill();
} else if (styleMode === 'rounded' || borderRadius > 0) {
const radius = (borderRadius / 100) * scale * 0.4;
ctx.beginPath();
ctx.roundRect(x, y, scale, scale, radius);
ctx.fill();
} else {
ctx.fillRect(x, y, scale, scale);
}
}
}
}
// 添加Logo
if (logoImage) {
const logoSize = (logoSizeEl.value / 100) * size;
const logoX = (canvas.width - logoSize) / 2;
const logoY = (canvas.height - logoSize) / 2;
// 白色背景圆形
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(logoX + logoSize/2, logoY + logoSize/2, logoSize/2 + 4, 0, 2 * Math.PI);
ctx.fill();
// 绘制Logo
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
}
}
// 辅助函数
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function filenameBase() {
const raw = (textEl.value || '').trim();
try { return new URL(raw).hostname.replace(/^www\./, ''); } catch { return raw.slice(0, 32).replace(/[^\w\-]+/g, '_') || 'qrcode'; }
}
// Logo处理
function handleLogoUpload(event) {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
logoImage = img;
generate();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
}
// 随机配色
function randomColors() {
darkEl.value = getRandomColor();
dark2El.value = getRandomColor();
lightEl.value = getRandomColor();
light2El.value = getRandomColor();
generate();
}
// 清除Logo
function clearLogo() {
logoImage = null;
logoEl.value = '';
generate();
}
// 链接测试
function testLink() {
const content = textEl.value.trim();
if (!content) {
alert('请先输入内容');
return;
}
linkPreview.textContent = content;
linkModal.style.display = 'block';
}
// 打开链接
function openLink() {
const content = textEl.value.trim();
if (isValidUrl(content)) {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
} else {
alert('不是有效的URL链接');
}
linkModal.style.display = 'none';
}
// 复制链接
async function copyLink() {
const content = textEl.value.trim();
try {
await navigator.clipboard.writeText(content);
alert('链接已复制到剪贴板');
} catch {
const t = document.createElement('textarea');
t.value = content;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
document.body.removeChild(t);
alert('链接已复制到剪贴板');
}
linkModal.style.display = 'none';
}
const generate = () => {
const content = (textEl.value || '').trim();
if (!content) {
svgWrap.innerHTML = '<em style="opacity:.7">请输入要编码的内容</em>';
return;
}
const level = levelEl.value || 'M';
const size = parseInt(sizeEl.value, 10) || 256;
const margin = parseInt(marginEl.value, 10) || 4;
const options = {
dark: darkEl.value || '#000000',
dark2: dark2El.value || '#4f46e5',
light: lightEl.value || '#ffffff',
light2: light2El.value || '#f3f4f6',
styleMode: styleModeEl.value || 'classic',
borderRadius: parseInt(borderRadiusEl.value, 10) || 0,
colorMode: colorModeEl.value || 'solid',
gradientDirection: parseInt(gradientDirectionEl.value, 10) || 0,
artEffect: artEffectEl.value || 'none',
animation: animationEl.value || 'none'
};
const qr = makeQRMatrix(content, level);
const svg = renderSVG(qr, size, margin, options);
// 应用艺术效果和动画
let finalSvg = svg;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = svg;
const svgElement = tempDiv.querySelector('svg');
if (options.artEffect !== 'none') {
svgElement.classList.add(`qr-${options.artEffect}`);
}
if (options.animation !== 'none') {
svgElement.classList.add(`qr-${options.animation}`);
}
svgWrap.innerHTML = tempDiv.innerHTML;
renderCanvas(qr, size, margin, options);
sizeOut.textContent = size;
radiusOut.textContent = options.borderRadius + '%';
logoSizeOut.textContent = logoSizeEl.value + '%';
// 自动跳转功能
if (autoRedirectEl.checked && isValidUrl(content)) {
svgWrap.style.cursor = 'pointer';
svgWrap.title = '点击访问链接';
svgWrap.onclick = () => {
if (newWindowEl.checked) {
window.open(content, '_blank');
} else {
window.location.href = content;
}
};
} else {
svgWrap.style.cursor = 'default';
svgWrap.title = '';
svgWrap.onclick = null;
}
};
// 下载 PNG
const downloadPNG = () => {
const a = document.createElement('a');
a.download = `${filenameBase()}.png`;
a.href = canvas.toDataURL('image/png');
a.click();
};
// 下载 SVG
const downloadSVG = () => {
const svgEl = svgWrap.querySelector('svg');
if (!svgEl) return;
const blob = new Blob([svgEl.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = `${filenameBase()}.svg`;
a.href = url;
a.click();
URL.revokeObjectURL(url);
};
// 复制 DataURL(PNG)
const copyDataURL = async () => {
const data = canvas.toDataURL('image/png');
try {
await navigator.clipboard.writeText(data);
alert('已复制 PNG DataURL 到剪贴板');
} catch {
// 回退:创建临时输入框
const t = document.createElement('textarea');
t.value = data; document.body.appendChild(t); t.select();
document.execCommand('copy'); document.body.removeChild(t);
alert('已复制 PNG DataURL 到剪贴板');
}
};
// 反色
const invert = () => {
const oldDark = darkEl.value, oldLight = lightEl.value;
darkEl.value = oldLight; lightEl.value = oldDark; generate();
};
// 事件绑定(带轻微防抖,输入体验更好)
textEl.addEventListener('input', debounce(generate, 180));
// 所有控制元素的变化事件
[levelEl, sizeEl, marginEl, darkEl, dark2El, lightEl, light2El,
styleModeEl, borderRadiusEl, colorModeEl, gradientDirectionEl,
logoSizeEl, artEffectEl, animationEl].forEach(el =>
el.addEventListener('input', generate)
);
// 按钮事件
$('#qr-download-png').addEventListener('click', downloadPNG);
$('#qr-download-svg').addEventListener('click', downloadSVG);
$('#qr-copy-dataurl').addEventListener('click', copyDataURL);
$('#qr-fill-url').addEventListener('click', () => { textEl.value = location.href; generate(); });
invertEl.addEventListener('click', invert);
$('#qr-random-colors').addEventListener('click', randomColors);
$('#qr-clear-logo').addEventListener('click', clearLogo);
$('#qr-test-link').addEventListener('click', testLink);
// Logo上传
logoEl.addEventListener('change', handleLogoUpload);
// 模态框事件
modalClose.addEventListener('click', () => { linkModal.style.display = 'none'; });
$('#qr-open-link').addEventListener('click', openLink);
$('#qr-copy-link').addEventListener('click', copyLink);
// 点击模态框外部关闭
window.addEventListener('click', (event) => {
if (event.target === linkModal) {
linkModal.style.display = 'none';
}
});
// 复选框事件
autoRedirectEl.addEventListener('change, generate);
newWindowEl.addEventListener('change', generate);
// 首次渲染
generate();
});
</script>