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 {
position: fixed;
bottom: 100px;
@@ -29,7 +29,7 @@
background: #3a7bc8;
}
/* 弹窗 */
/* ========== 弹窗 ========== */
#qr-modal {
display: none;
position: fixed;
@@ -128,9 +128,32 @@
color: #fff;
}
/* 画布上的二维码 */
/* ========== 画布上的二维码 ========== */
.qr-item {
position: absolute;
cursor: move;
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>
<button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button>
</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">
<label class="toolbar-label">

373
js/qr.js
View File

@@ -1,153 +1,305 @@
// 二维码功能 - 简洁可靠
// 二维码功能 - 优化版编辑器工具栏
(function() {
'use strict';
// 获取元素
const btn = document.getElementById('qr-btn');
const modal = document.getElementById('qr-modal');
const input = document.getElementById('qr-input');
const preview = document.getElementById('qr-preview');
const closeBtn = document.querySelector('.qr-close');
const cancelBtn = document.querySelector('.qr-cancel');
const okBtn = document.querySelector('.qr-ok');
// ========== 配置 ==========
const CONFIG = {
MIN_SIZE: 0,
MAX_SIZE: 200,
DEFAULT_SIZE: 100,
PREVIEW_SIZE: 128,
DEBOUNCE_DELAY: 300
};
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');
if (!container) return;
// 排除二维码图片,只检查背景图
const img = container.querySelector('img:not([data-type="qrcode"])');
if (img && img.src && !img.src.endsWith(window.location.href)) {
btn.classList.add('show');
} else {
btn.classList.remove('show');
}
const bgImg = container.querySelector('img:not([data-type="qrcode"])');
const hasBackground = bgImg && bgImg.src && !bgImg.src.endsWith(window.location.href);
elements.btn.classList.toggle('show', hasBackground);
}
// 监听背景图变化
const observer = new MutationObserver(checkBg);
const container = document.getElementById('imageContainer');
if (container) {
observer.observe(container, { childList: true, subtree: true });
}
setInterval(checkBg, 500);
// 打开弹窗
/**
* 打开二维码生成弹窗
*/
function openModal() {
modal.classList.add('active');
input.value = '';
preview.innerHTML = '';
currentQR = null;
input.focus();
elements.modal.classList.add('active');
elements.input.value = '';
elements.preview.innerHTML = '';
state.currentQR = null;
elements.input.focus();
}
// 关闭弹窗
/**
* 关闭二维码生成弹窗
*/
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) {
preview.innerHTML = '';
currentQR = null;
elements.preview.innerHTML = '';
state.currentQR = null;
return;
}
preview.innerHTML = '';
elements.preview.innerHTML = '';
try {
currentQR = new QRCode(preview, {
state.currentQR = new QRCode(elements.preview, {
text: text,
width: 128,
height: 128
width: CONFIG.PREVIEW_SIZE,
height: CONFIG.PREVIEW_SIZE,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
} catch (e) {
console.error('生成失败', e);
} catch (error) {
console.error('二维码生成失败:', error);
}
}
// 添加到画布
/**
* 将二维码添加到画布
*/
function addToCanvas() {
if (!currentQR) {
if (!state.currentQR) {
alert('请输入内容');
return;
}
const canvas = preview.querySelector('canvas');
if (!canvas) return;
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'qr-item';
img.dataset.type = 'qrcode';
img.style.width = '100px';
img.style.height = '100px';
const canvas = elements.preview.querySelector('canvas');
if (!canvas) return;
const container = document.getElementById('imageContainer');
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);
makeDraggable(img);
selectQR(img);
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) {
let dragging = false;
let isDragging = false;
let startX, startY, startLeft, startTop;
function startDrag(clientX, clientY) {
dragging = true;
isDragging = true;
startX = clientX;
startY = clientY;
startLeft = parseInt(el.style.left) || 0;
startTop = parseInt(el.style.top) || 0;
el.style.cursor = 'grabbing';
}
function moveDrag(clientX, clientY) {
if (!dragging) return;
if (!isDragging) return;
el.style.left = (startLeft + clientX - startX) + 'px';
el.style.top = (startTop + clientY - startY) + 'px';
}
function endDrag() {
if (dragging) {
dragging = false;
if (typeof saveState === 'function') saveState();
if (isDragging) {
isDragging = false;
el.style.cursor = 'move';
saveState();
}
}
// 鼠标事件
el.addEventListener('mousedown', function(e) {
el.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
selectQR(el);
startDrag(e.clientX, e.clientY);
});
// 触摸事件
el.addEventListener('touchstart', function(e) {
el.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
selectQR(el);
if (e.touches.length === 1) {
startDrag(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
// 全局移动事件
document.addEventListener('mousemove', function(e) {
moveDrag(e.clientX, e.clientY);
});
document.addEventListener('touchmove', function(e) {
if (dragging && e.touches.length === 1) {
document.addEventListener('mousemove', (e) => moveDrag(e.clientX, e.clientY));
document.addEventListener('touchmove', (e) => {
if (isDragging && e.touches.length === 1) {
e.preventDefault();
moveDrag(e.touches[0].clientX, e.touches[0].clientY);
}
@@ -158,22 +310,67 @@
document.addEventListener('touchend', endDrag);
}
// 事件绑定
btn.addEventListener('click', openModal);
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
okBtn.addEventListener('click', addToCanvas);
modal.addEventListener('click', function(e) {
if (e.target === modal) closeModal();
// ========== 事件绑定 ==========
function bindEvents() {
// 弹窗事件
elements.btn.addEventListener('click', openModal);
elements.closeBtn.addEventListener('click', closeModal);
elements.cancelBtn.addEventListener('click', closeModal);
elements.okBtn.addEventListener('click', addToCanvas);
elements.modal.addEventListener('click', (e) => {
if (e.target === elements.modal) closeModal();
});
// 输入防抖
let timer;
input.addEventListener('input', function() {
clearTimeout(timer);
timer = setTimeout(genPreview, 300);
elements.input.addEventListener('input', () => {
clearTimeout(state.previewTimer);
state.previewTimer = setTimeout(generatePreview, CONFIG.DEBOUNCE_DELAY);
});
// 透明度滑块
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');
const isOverlayImage = e.target.closest('.overlay-image');
const isTextInput = e.target.closest('.text-input');
const isQRCode = e.target.closest('.qr-item');
// 点击背景/空白区域
if (isBackground && !isOverlayImage) {
// 点击背景/空白区域(排除二维码)
if (isBackground && !isOverlayImage && !isQRCode) {
// 如果正在编辑,先结束编辑
if (editingInput) {
endEdit(editingInput);
@@ -398,18 +399,26 @@
selectedOverlay.classList.remove('selected');
selectedOverlay = null;
}
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
// 隐藏辅助线
hideGuides();
// 隐藏编辑器工具栏
editorToolbar.classList.remove('active');
}
// 点击空白区域(不是层叠图和输入框)
if (!isOverlayImage && !isTextInput && !isBackground) {
// 点击空白区域(不是层叠图、输入框和二维码)
if (!isOverlayImage && !isTextInput && !isBackground && !isQRCode) {
if (selectedOverlay) {
selectedOverlay.classList.remove('selected');
selectedOverlay = null;
}
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
}
}
@@ -1198,6 +1207,9 @@
textTools.classList.remove('active');
imageTools.classList.add('active');
bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新透明度滑块为当前层叠图的透明度
const currentOpacity = overlayEl.style.opacity || '1';
@@ -1371,6 +1383,9 @@
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
@@ -1394,6 +1409,9 @@
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
@@ -1444,6 +1462,9 @@
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
@@ -1701,6 +1722,9 @@
textTools.classList.add('active');
imageTools.classList.remove('active');
bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新工具栏状态
updateToolbarState(element);
@@ -1724,6 +1748,9 @@
textTools.classList.add('active');
imageTools.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) {
@@ -2666,6 +2693,9 @@
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
}
}