feat(qr): 添加二维码工具栏并优化交互体验

添加二维码编辑工具栏,支持透明度和缩放调整
优化选中状态和拖拽交互,增强用户体验
重构代码结构,提高可维护性
This commit is contained in:
Zhukj
2026-04-25 16:51:27 +08:00
parent 37aaec1e42
commit c7963cec6e
4 changed files with 363 additions and 99 deletions

View File

@@ -1,6 +1,6 @@
/* 二维码功能样式 */ /* 二维码功能样式 */
/* 悬浮按钮 */ /* ========== 悬浮按钮 ========== */
#qr-btn { #qr-btn {
position: fixed; position: fixed;
bottom: 100px; bottom: 100px;
@@ -29,7 +29,7 @@
background: #3a7bc8; background: #3a7bc8;
} }
/* 弹窗 */ /* ========== 弹窗 ========== */
#qr-modal { #qr-modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -128,9 +128,32 @@
color: #fff; color: #fff;
} }
/* 画布上的二维码 */ /* ========== 画布上的二维码 ========== */
.qr-item { .qr-item {
position: absolute; position: absolute;
cursor: move; cursor: move;
user-select: none; user-select: none;
touch-action: none;
}
/* 选中状态 */
.qr-item.qr-selected {
outline: 2px solid #4a90e2;
outline-offset: 2px;
}
/* ========== 二维码工具栏 - 与背景图工具栏保持一致 ========== */
.qr-tools {
display: none;
}
.qr-tools.active {
display: flex;
}
/* 数值显示固定宽度,单独占位 */
#qrOpacityValue, #qrScaleValue {
display: inline-block;
min-width: 40px;
text-align: center;
} }

View File

@@ -74,6 +74,20 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button> <button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button>
</div> </div>
<!-- 二维码编辑工具 -->
<div class="toolbar-group qr-tools" id="qrTools">
<label class="toolbar-label">
<span>透明度</span>
<input type="range" id="qrOpacitySlider" min="0" max="100" value="100" class="opacity-slider">
<span id="qrOpacityValue">100%</span>
</label>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
<span>缩放</span>
<input type="range" id="qrScaleSlider" min="0" max="200" value="100" class="scale-slider">
<span id="qrScaleValue">100%</span>
</label>
</div>
<!-- 背景图编辑工具 --> <!-- 背景图编辑工具 -->
<div class="toolbar-group bg-tools" id="bgTools"> <div class="toolbar-group bg-tools" id="bgTools">
<label class="toolbar-label"> <label class="toolbar-label">

373
js/qr.js
View File

@@ -1,153 +1,305 @@
// 二维码功能 - 简洁可靠 // 二维码功能 - 优化版编辑器工具栏
(function() { (function() {
'use strict'; 'use strict';
// 获取元素 // ========== 配置 ==========
const btn = document.getElementById('qr-btn'); const CONFIG = {
const modal = document.getElementById('qr-modal'); MIN_SIZE: 0,
const input = document.getElementById('qr-input'); MAX_SIZE: 200,
const preview = document.getElementById('qr-preview'); DEFAULT_SIZE: 100,
const closeBtn = document.querySelector('.qr-close'); PREVIEW_SIZE: 128,
const cancelBtn = document.querySelector('.qr-cancel'); DEBOUNCE_DELAY: 300
const okBtn = document.querySelector('.qr-ok'); };
if (!btn || !modal) return; // ========== DOM 元素缓存 ==========
const elements = {
btn: document.getElementById('qr-btn'),
modal: document.getElementById('qr-modal'),
input: document.getElementById('qr-input'),
preview: document.getElementById('qr-preview'),
closeBtn: document.querySelector('.qr-close'),
cancelBtn: document.querySelector('.qr-cancel'),
okBtn: document.querySelector('.qr-ok'),
// 工具栏
editorToolbar: document.getElementById('editorToolbar'),
textTools: document.getElementById('textTools'),
imageTools: document.getElementById('imageTools'),
bgTools: document.getElementById('bgTools'),
qrTools: document.getElementById('qrTools'),
qrOpacitySlider: document.getElementById('qrOpacitySlider'),
qrOpacityValue: document.getElementById('qrOpacityValue'),
qrScaleSlider: document.getElementById('qrScaleSlider'),
qrScaleValue: document.getElementById('qrScaleValue')
};
let currentQR = null; // 验证必需元素
let qrCount = 0; if (!elements.btn || !elements.modal) return;
// 检查背景图并显示按钮 // ========== 状态管理 ==========
function checkBg() { const state = {
currentQR: null,
selectedQR: null,
previewTimer: null,
isDragging: false
};
// ========== 核心功能 ==========
/**
* 检查背景图并显示/隐藏二维码按钮
*/
function checkBackground() {
const container = document.getElementById('imageContainer'); const container = document.getElementById('imageContainer');
if (!container) return; if (!container) return;
// 排除二维码图片,只检查背景图
const img = container.querySelector('img:not([data-type="qrcode"])'); const bgImg = container.querySelector('img:not([data-type="qrcode"])');
if (img && img.src && !img.src.endsWith(window.location.href)) { const hasBackground = bgImg && bgImg.src && !bgImg.src.endsWith(window.location.href);
btn.classList.add('show');
} else { elements.btn.classList.toggle('show', hasBackground);
btn.classList.remove('show');
}
} }
// 监听背景图变化 /**
const observer = new MutationObserver(checkBg); * 打开二维码生成弹窗
const container = document.getElementById('imageContainer'); */
if (container) {
observer.observe(container, { childList: true, subtree: true });
}
setInterval(checkBg, 500);
// 打开弹窗
function openModal() { function openModal() {
modal.classList.add('active'); elements.modal.classList.add('active');
input.value = ''; elements.input.value = '';
preview.innerHTML = ''; elements.preview.innerHTML = '';
currentQR = null; state.currentQR = null;
input.focus(); elements.input.focus();
} }
// 关闭弹窗 /**
* 关闭二维码生成弹窗
*/
function closeModal() { function closeModal() {
modal.classList.remove('active'); elements.modal.classList.remove('active');
} }
// 生成预览 /**
function genPreview() { * 生成二维码预览
const text = input.value.trim(); */
function generatePreview() {
const text = elements.input.value.trim();
if (!text) { if (!text) {
preview.innerHTML = ''; elements.preview.innerHTML = '';
currentQR = null; state.currentQR = null;
return; return;
} }
preview.innerHTML = '';
elements.preview.innerHTML = '';
try { try {
currentQR = new QRCode(preview, { state.currentQR = new QRCode(elements.preview, {
text: text, text: text,
width: 128, width: CONFIG.PREVIEW_SIZE,
height: 128 height: CONFIG.PREVIEW_SIZE,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
}); });
} catch (e) { } catch (error) {
console.error('生成失败', e); console.error('二维码生成失败:', error);
} }
} }
// 添加到画布 /**
* 将二维码添加到画布
*/
function addToCanvas() { function addToCanvas() {
if (!currentQR) { if (!state.currentQR) {
alert('请输入内容'); alert('请输入内容');
return; return;
} }
const canvas = preview.querySelector('canvas');
if (!canvas) return;
const img = document.createElement('img'); const canvas = elements.preview.querySelector('canvas');
img.src = canvas.toDataURL(); if (!canvas) return;
img.className = 'qr-item';
img.dataset.type = 'qrcode';
img.style.width = '100px';
img.style.height = '100px';
const container = document.getElementById('imageContainer'); const container = document.getElementById('imageContainer');
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
img.style.left = (rect.width / 2 - 50) + 'px';
img.style.top = (rect.height / 2 - 50) + 'px'; const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.className = 'qr-item';
img.dataset.type = 'qrcode';
img.style.cssText = `
width: ${CONFIG.DEFAULT_SIZE}px;
height: ${CONFIG.DEFAULT_SIZE}px;
opacity: 1;
left: ${(rect.width / 2 - CONFIG.DEFAULT_SIZE / 2)}px;
top: ${(rect.height / 2 - CONFIG.DEFAULT_SIZE / 2)}px;
`;
container.appendChild(img); container.appendChild(img);
makeDraggable(img); makeDraggable(img);
selectQR(img);
closeModal(); closeModal();
if (typeof saveState === 'function') saveState(); saveState();
} }
// 拖拽功能 - 支持鼠标和触摸 /**
* 选中二维码
*/
function selectQR(img) {
// 取消其他选中状态
document.querySelectorAll('.qr-item').forEach(el => {
el.classList.remove('qr-selected');
});
// 选中当前二维码
img.classList.add('qr-selected');
state.selectedQR = img;
// 显示二维码工具栏
showQRTools();
}
/**
* 显示二维码工具栏
*/
function showQRTools() {
if (!state.selectedQR) return;
// 激活编辑器工具栏
if (elements.editorToolbar) {
elements.editorToolbar.classList.add('active');
}
// 隐藏其他工具栏
['textTools', 'imageTools', 'bgTools'].forEach(key => {
if (elements[key]) elements[key].classList.remove('active');
});
// 显示二维码工具栏
if (elements.qrTools) {
elements.qrTools.classList.add('active');
}
// 同步滑块值
syncToolbarValues();
}
/**
* 同步工具栏滑块值
*/
function syncToolbarValues() {
if (!state.selectedQR) return;
const opacity = parseFloat(state.selectedQR.style.opacity) || 1;
const size = parseInt(state.selectedQR.style.width) || CONFIG.DEFAULT_SIZE;
if (elements.qrOpacitySlider) {
elements.qrOpacitySlider.value = Math.round(opacity * 100);
elements.qrOpacityValue.textContent = Math.round(opacity * 100) + '%';
}
if (elements.qrScaleSlider) {
elements.qrScaleSlider.value = size;
elements.qrScaleValue.textContent = size + '%';
}
}
/**
* 隐藏二维码工具栏
*/
function hideQRTools() {
if (elements.qrTools) {
elements.qrTools.classList.remove('active');
}
document.querySelectorAll('.qr-item').forEach(el => {
el.classList.remove('qr-selected');
});
state.selectedQR = null;
}
/**
* 更新二维码透明度
*/
function updateOpacity(value) {
if (!state.selectedQR) return;
state.selectedQR.style.opacity = value / 100;
elements.qrOpacityValue.textContent = value + '%';
}
/**
* 更新二维码大小
*/
function updateScale(value) {
if (!state.selectedQR) return;
state.selectedQR.style.width = value + 'px';
state.selectedQR.style.height = value + 'px';
elements.qrScaleValue.textContent = value + '%';
}
/**
* 保存状态
*/
function saveState() {
if (typeof window.saveState === 'function') {
window.saveState();
}
}
// ========== 拖拽功能 ==========
/**
* 使元素可拖拽
*/
function makeDraggable(el) { function makeDraggable(el) {
let dragging = false; let isDragging = false;
let startX, startY, startLeft, startTop; let startX, startY, startLeft, startTop;
function startDrag(clientX, clientY) { function startDrag(clientX, clientY) {
dragging = true; isDragging = true;
startX = clientX; startX = clientX;
startY = clientY; startY = clientY;
startLeft = parseInt(el.style.left) || 0; startLeft = parseInt(el.style.left) || 0;
startTop = parseInt(el.style.top) || 0; startTop = parseInt(el.style.top) || 0;
el.style.cursor = 'grabbing';
} }
function moveDrag(clientX, clientY) { function moveDrag(clientX, clientY) {
if (!dragging) return; if (!isDragging) return;
el.style.left = (startLeft + clientX - startX) + 'px'; el.style.left = (startLeft + clientX - startX) + 'px';
el.style.top = (startTop + clientY - startY) + 'px'; el.style.top = (startTop + clientY - startY) + 'px';
} }
function endDrag() { function endDrag() {
if (dragging) { if (isDragging) {
dragging = false; isDragging = false;
if (typeof saveState === 'function') saveState(); el.style.cursor = 'move';
saveState();
} }
} }
// 鼠标事件 // 鼠标事件
el.addEventListener('mousedown', function(e) { el.addEventListener('mousedown', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
selectQR(el);
startDrag(e.clientX, e.clientY); startDrag(e.clientX, e.clientY);
}); });
// 触摸事件 // 触摸事件
el.addEventListener('touchstart', function(e) { el.addEventListener('touchstart', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
selectQR(el);
if (e.touches.length === 1) { if (e.touches.length === 1) {
startDrag(e.touches[0].clientX, e.touches[0].clientY); startDrag(e.touches[0].clientX, e.touches[0].clientY);
} }
}, { passive: false }); }, { passive: false });
// 全局移动事件 // 全局移动事件
document.addEventListener('mousemove', function(e) { document.addEventListener('mousemove', (e) => moveDrag(e.clientX, e.clientY));
moveDrag(e.clientX, e.clientY); document.addEventListener('touchmove', (e) => {
}); if (isDragging && e.touches.length === 1) {
document.addEventListener('touchmove', function(e) {
if (dragging && e.touches.length === 1) {
e.preventDefault(); e.preventDefault();
moveDrag(e.touches[0].clientX, e.touches[0].clientY); moveDrag(e.touches[0].clientX, e.touches[0].clientY);
} }
@@ -158,22 +310,67 @@
document.addEventListener('touchend', endDrag); document.addEventListener('touchend', endDrag);
} }
// 事件绑定 // ========== 事件绑定 ==========
btn.addEventListener('click', openModal);
closeBtn.addEventListener('click', closeModal); function bindEvents() {
cancelBtn.addEventListener('click', closeModal); // 弹窗事件
okBtn.addEventListener('click', addToCanvas); elements.btn.addEventListener('click', openModal);
modal.addEventListener('click', function(e) { elements.closeBtn.addEventListener('click', closeModal);
if (e.target === modal) closeModal(); elements.cancelBtn.addEventListener('click', closeModal);
elements.okBtn.addEventListener('click', addToCanvas);
elements.modal.addEventListener('click', (e) => {
if (e.target === elements.modal) closeModal();
}); });
// 输入防抖 // 输入防抖
let timer; elements.input.addEventListener('input', () => {
input.addEventListener('input', function() { clearTimeout(state.previewTimer);
clearTimeout(timer); state.previewTimer = setTimeout(generatePreview, CONFIG.DEBOUNCE_DELAY);
timer = setTimeout(genPreview, 300);
}); });
// 透明度滑块
if (elements.qrOpacitySlider) {
elements.qrOpacitySlider.addEventListener('input', (e) => {
updateOpacity(e.target.value);
});
elements.qrOpacitySlider.addEventListener('change', saveState);
}
// 缩放滑块
if (elements.qrScaleSlider) {
elements.qrScaleSlider.addEventListener('input', (e) => {
updateScale(e.target.value);
});
elements.qrScaleSlider.addEventListener('change', saveState);
}
}
// ========== 初始化 ==========
function init() {
// 监听背景图变化
const container = document.getElementById('imageContainer');
if (container) {
const observer = new MutationObserver(checkBackground);
observer.observe(container, { childList: true, subtree: true });
}
// 定期检查背景图状态
setInterval(checkBackground, 500);
// 绑定事件
bindEvents();
// 初始检查 // 初始检查
checkBg(); checkBackground();
// 暴露全局 API
window.QRCodeAPI = {
hideTools: hideQRTools,
getSelected: () => state.selectedQR
};
}
// 启动
init();
})(); })();

View File

@@ -381,9 +381,10 @@
e.target === document.getElementById('emptyTip'); e.target === document.getElementById('emptyTip');
const isOverlayImage = e.target.closest('.overlay-image'); const isOverlayImage = e.target.closest('.overlay-image');
const isTextInput = e.target.closest('.text-input'); const isTextInput = e.target.closest('.text-input');
const isQRCode = e.target.closest('.qr-item');
// 点击背景/空白区域 // 点击背景/空白区域(排除二维码)
if (isBackground && !isOverlayImage) { if (isBackground && !isOverlayImage && !isQRCode) {
// 如果正在编辑,先结束编辑 // 如果正在编辑,先结束编辑
if (editingInput) { if (editingInput) {
endEdit(editingInput); endEdit(editingInput);
@@ -398,18 +399,26 @@
selectedOverlay.classList.remove('selected'); selectedOverlay.classList.remove('selected');
selectedOverlay = null; selectedOverlay = null;
} }
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
// 隐藏辅助线 // 隐藏辅助线
hideGuides(); hideGuides();
// 隐藏编辑器工具栏 // 隐藏编辑器工具栏
editorToolbar.classList.remove('active'); editorToolbar.classList.remove('active');
} }
// 点击空白区域(不是层叠图和输入框) // 点击空白区域(不是层叠图、输入框和二维码)
if (!isOverlayImage && !isTextInput && !isBackground) { if (!isOverlayImage && !isTextInput && !isBackground && !isQRCode) {
if (selectedOverlay) { if (selectedOverlay) {
selectedOverlay.classList.remove('selected'); selectedOverlay.classList.remove('selected');
selectedOverlay = null; selectedOverlay = null;
} }
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
} }
} }
@@ -1198,6 +1207,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.add('active'); imageTools.classList.add('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新透明度滑块为当前层叠图的透明度 // 更新透明度滑块为当前层叠图的透明度
const currentOpacity = overlayEl.style.opacity || '1'; const currentOpacity = overlayEl.style.opacity || '1';
@@ -1371,6 +1383,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1394,6 +1409,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1444,6 +1462,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1701,6 +1722,9 @@
textTools.classList.add('active'); textTools.classList.add('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新工具栏状态 // 更新工具栏状态
updateToolbarState(element); updateToolbarState(element);
@@ -1724,6 +1748,9 @@
textTools.classList.add('active'); textTools.classList.add('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 移动端/编辑模式:固定在顶部始终可见 // 移动端/编辑模式:固定在顶部始终可见
if (window.innerWidth <= 768 || 'ontouchstart' in window) { if (window.innerWidth <= 768 || 'ontouchstart' in window) {
@@ -2666,6 +2693,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
} }
} }