From 1983c15fb9404836f289acf1d5a538f6b6e9b149 Mon Sep 17 00:00:00 2001 From: Zhukj <3262118957> Date: Wed, 22 Apr 2026 14:08:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0=E6=92=A4?= =?UTF-8?q?=E9=94=80=E9=87=8D=E5=81=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现完整的撤销重做功能,包括: 1. 添加工具栏撤销/重做按钮及快捷键支持 2. 实现状态保存与恢复机制 3. 添加状态变化检测与历史记录管理 4. 更新相关UI交互以触发状态保存 --- css/style.css | 37 +++++ index.html | 4 + js/script.js | 387 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 428 insertions(+) diff --git a/css/style.css b/css/style.css index 3993103..c03ab43 100644 --- a/css/style.css +++ b/css/style.css @@ -62,6 +62,43 @@ html, body { background: #3a7bc8; } +/* 工具栏垂直分隔线 */ +.toolbar-divider-v { + width: 1px; + height: 24px; + background: #ddd; + margin: 0 4px; +} + +/* 工具栏图标按钮 */ +.toolbar-btn-icon { + width: 32px; + height: 32px; + font-size: 16px; + border: 1px solid #ddd; + border-radius: 6px; + background: #fff; + color: #333; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.toolbar-btn-icon:hover { + background: #f5f5f5; +} + +.toolbar-btn-icon:active { + background: #e8e8e8; +} + +.toolbar-btn-icon:disabled { + opacity: 0.4; + cursor: not-allowed; +} + /* 文本输入框删除按钮 */ .text-input .text-delete-btn { position: absolute; diff --git a/index.html b/index.html index 3a43806..8f07814 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,10 @@ +
+ + +
diff --git a/js/script.js b/js/script.js index 8895f3e..7dcd68a 100644 --- a/js/script.js +++ b/js/script.js @@ -60,6 +60,15 @@ let maxOffsetX = 0; let isInteractingWithBgImage = false; + // 撤销重做相关变量 + const undoStack = []; + const redoStack = []; + const MAX_HISTORY = 50; + let isUndoing = false; + let saveStateTimer = null; + let lastSavedState = null; + const SAVE_DELAY = 300; // 防抖延迟 300ms + // 初始化 init(); @@ -100,6 +109,23 @@ // 保存按钮 saveBtn.addEventListener('click', saveImage); + // 撤销重做按钮 + const undoBtn = document.getElementById('undoBtn'); + const redoBtn = document.getElementById('redoBtn'); + + undoBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + undo(); + }); + redoBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + redo(); + }); + + updateUndoRedoButtons(); + // 初始化输入框 initInput(input1); initInput(input2); @@ -127,6 +153,9 @@ opacityValue.textContent = e.target.value + '%'; } }); + opacitySlider.addEventListener('change', () => { + saveState(); + }); // 旋转滑块 rotateSlider.addEventListener('input', (e) => { @@ -150,6 +179,9 @@ rotateValue.textContent = rotateDeg + '°'; } }); + rotateSlider.addEventListener('change', () => { + saveState(); + }); // 水平镜像按钮 mirrorHBtn.addEventListener('click', () => { @@ -171,6 +203,9 @@ } img.style.transform = newTransform; + + // 保存状态 + saveState(); } } }); @@ -181,6 +216,9 @@ bgScaleValue.textContent = e.target.value + '%'; updateBgImage(); }); + bgScaleSlider.addEventListener('change', () => { + saveState(); + }); // 背景图透明度滑块 bgOpacitySlider.addEventListener('input', (e) => { @@ -188,6 +226,9 @@ bgOpacityValue.textContent = e.target.value + '%'; bgImage.style.opacity = bgImageOpacity; }); + bgOpacitySlider.addEventListener('change', () => { + saveState(); + }); // 点击空白区域取消选中 document.addEventListener('click', (e) => { @@ -273,9 +314,301 @@ // 加载默认背景图 loadDefaultBackground(); + // 保存初始状态 + setTimeout(() => { + saveState(); + }, 500); } + // ==================== 撤销重做功能 ==================== + + // 保存当前状态(带防抖) + function saveState(immediate = false) { + if (isUndoing) return; + + // 清除之前的定时器 + if (saveStateTimer) { + clearTimeout(saveStateTimer); + saveStateTimer = null; + } + + const doSave = () => { + const state = captureState(); + + // 如果新状态与上一个状态相同,不保存 + if (lastSavedState && statesEqual(state, lastSavedState)) { + return; + } + + undoStack.push(state); + lastSavedState = JSON.parse(JSON.stringify(state)); + + // 限制历史记录数量 + if (undoStack.length > MAX_HISTORY) { + undoStack.shift(); + } + + // 清空重做栈 + redoStack.length = 0; + + updateUndoRedoButtons(); + }; + + if (immediate) { + doSave(); + } else { + saveStateTimer = setTimeout(doSave, SAVE_DELAY); + } + } + + // 捕获当前状态 + function captureState() { + return { + // 背景图状态 + bgImageSrc: bgImage.src, + bgImageScale: bgImageScale, + bgImageOpacity: bgImageOpacity, + imageOffsetX: imageOffsetX, + + // 层叠图状态 + overlays: Array.from(overlayLayer.querySelectorAll('.overlay-image')).map(el => { + const img = el.querySelector('img'); + return { + src: img ? img.src : '', + left: el.style.left, + top: el.style.top, + width: el.style.width, + height: el.style.height, + opacity: el.style.opacity, + transform: el.style.transform, + imgTransform: img ? img.style.transform : '' + }; + }), + + // 文本输入框状态 + textInputs: Array.from(document.querySelectorAll('.preview-input-layer .text-input')).map(el => ({ + id: el.id, + content: el.innerText.replace('×', '').trim(), + left: el.style.left, + top: el.style.top, + color: el.style.color, + fontFamily: el.style.fontFamily, + fontSize: el.style.fontSize, + fontWeight: el.style.fontWeight, + fontStyle: el.style.fontStyle, + isLocked: el.dataset.locked === 'true' + })) + }; + } + + // 比较两个状态是否相同 + function statesEqual(state1, state2) { + return JSON.stringify(state1) === JSON.stringify(state2); + } + + // 撤销 + function undo() { + if (undoStack.length <= 1) return; + + isUndoing = true; + + // 将当前状态移到重做栈 + const currentState = undoStack.pop(); + redoStack.push(currentState); + + // 恢复上一个状态 + const prevState = undoStack[undoStack.length - 1]; + restoreState(prevState); + + updateUndoRedoButtons(); + + isUndoing = false; + } + + // 重做 + function redo() { + if (redoStack.length === 0) return; + + isUndoing = true; + + // 从重做栈恢复状态 + const state = redoStack.pop(); + undoStack.push(state); + + restoreState(state); + + updateUndoRedoButtons(); + + isUndoing = false; + } + + // 恢复状态 + function restoreState(state) { + // 恢复背景图 + if (state.bgImageSrc) { + if (state.bgImageSrc !== bgImage.src) { + // 创建新图片对象确保加载完成 + const img = new Image(); + img.onload = () => { + currentImage = img; + bgImage.src = state.bgImageSrc; + bgImageScale = state.bgImageScale; + bgImageOpacity = state.bgImageOpacity; + imageOffsetX = state.imageOffsetX; + + bgScaleSlider.value = Math.round(bgImageScale * 100); + bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; + bgOpacitySlider.value = Math.round(bgImageOpacity * 100); + bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%'; + + updateBgImage(); + + // 更新UI状态 + imageContainer.classList.remove('empty'); + emptyTip.classList.add('hidden'); + }; + img.src = state.bgImageSrc; + } else { + // 相同的图片,直接更新参数 + bgImageScale = state.bgImageScale; + bgImageOpacity = state.bgImageOpacity; + imageOffsetX = state.imageOffsetX; + + bgScaleSlider.value = Math.round(bgImageScale * 100); + bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; + bgOpacitySlider.value = Math.round(bgImageOpacity * 100); + bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%'; + + updateBgImage(); + } + } else { + // 没有背景图,清空 + bgImage.src = ''; + currentImage = null; + imageContainer.classList.add('empty'); + emptyTip.classList.remove('hidden'); + } + + // 恢复层叠图 + overlayLayer.innerHTML = ''; + state.overlays.forEach(overlayData => { + const div = document.createElement('div'); + div.className = 'overlay-image'; + div.style.left = overlayData.left; + div.style.top = overlayData.top; + div.style.width = overlayData.width; + div.style.height = overlayData.height; + div.style.opacity = overlayData.opacity; + div.style.transform = overlayData.transform; + + const img = document.createElement('img'); + img.src = overlayData.src; + img.style.transform = overlayData.imgTransform; + img.draggable = false; + + div.appendChild(img); + overlayLayer.appendChild(div); + + // 重新初始化拖拽 + initOverlayDrag(div); + }); + + // 恢复文本输入框 + const inputLayer = document.getElementById('previewInputLayer'); + + // 先隐藏所有预设输入框 + [input1, input2, input3].forEach(inputEl => { + if (inputEl) { + inputEl.classList.add('hidden'); + } + }); + + // 清除动态添加的输入框(保留预设的三个) + const allInputs = inputLayer.querySelectorAll('.text-input'); + allInputs.forEach(input => { + if (input.id !== 'input1' && input.id !== 'input2' && input.id !== 'input3') { + input.remove(); + } + }); + + state.textInputs.forEach(inputData => { + let input = document.getElementById(inputData.id); + if (!input) { + // 创建新的输入框 + input = document.createElement('div'); + input.id = inputData.id; + input.className = 'text-input'; + input.contentEditable = false; + input.dataset.placeholder = '请输入'; + inputLayer.appendChild(input); + initInput(input); + } + + // 显示输入框 + input.classList.remove('hidden'); + + // 清除现有内容(保留删除按钮) + const deleteBtn = input.querySelector('.text-delete-btn'); + input.innerHTML = ''; + + // 设置文本内容 + input.appendChild(document.createTextNode(inputData.content)); + + // 重新添加删除按钮 + if (deleteBtn) { + input.appendChild(deleteBtn); + } else { + const newDeleteBtn = document.createElement('button'); + newDeleteBtn.className = 'text-delete-btn'; + newDeleteBtn.innerHTML = '×'; + newDeleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + input.remove(); + saveState(); + }); + input.appendChild(newDeleteBtn); + } + + // 应用样式 + input.style.left = inputData.left; + input.style.top = inputData.top; + input.style.color = inputData.color || '#000000'; + input.style.fontFamily = inputData.fontFamily || ''; + input.style.fontSize = inputData.fontSize || ''; + input.style.fontWeight = inputData.fontWeight || 'bold'; + input.style.fontStyle = inputData.fontStyle || 'normal'; + input.dataset.locked = inputData.isLocked ? 'true' : ''; + + // 更新锁定样式 + if (inputData.isLocked) { + input.classList.add('locked'); + } else { + input.classList.remove('locked'); + } + + // 检查是否需要显示 placeholder + if (!inputData.content.trim()) { + input.classList.add('show-placeholder'); + } else { + input.classList.remove('show-placeholder'); + } + }); + } + + // 更新撤销重做按钮状态 + function updateUndoRedoButtons() { + const undoBtn = document.getElementById('undoBtn'); + const redoBtn = document.getElementById('redoBtn'); + + if (undoBtn) { + undoBtn.disabled = undoStack.length <= 1; + } + if (redoBtn) { + redoBtn.disabled = redoStack.length === 0; + } + } + // 加载默认背景图 function loadDefaultBackground() { const defaultBgUrl = './images/default-bg.jpg'; @@ -334,6 +667,9 @@ // 显示预设的文本输入框 showPresetTextInputs(); + + // 保存状态(立即) + saveState(true); }; img.src = e.target.result; }; @@ -381,6 +717,9 @@ initInput(textEl); selectInput(textEl); + + // 保存状态(立即) + saveState(true); } // 加载层叠图 @@ -436,6 +775,8 @@ deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); overlayEl.remove(); + // 保存状态(立即) + saveState(true); }); overlayEl.appendChild(deleteBtn); @@ -452,6 +793,9 @@ // 选中新添加的层叠图 selectOverlay(overlayEl); + + // 保存状态(立即) + saveState(true); } // 初始化层叠图交互 @@ -534,6 +878,9 @@ document.removeEventListener('mouseup', handleDragEnd); document.removeEventListener('touchmove', handleDragMove); document.removeEventListener('touchend', handleDragEnd); + + // 保存状态 + saveState(); } // 缩放控制(四角) @@ -633,6 +980,9 @@ document.removeEventListener('mouseup', endResize); document.removeEventListener('touchmove', handleResize); document.removeEventListener('touchend', endResize); + + // 保存状态 + saveState(); } }); } @@ -935,6 +1285,10 @@ } function handleImageEnd() { + if (isDraggingImage) { + // 保存状态 + saveState(); + } isDraggingImage = false; isInteractingWithBgImage = false; } @@ -1098,6 +1452,9 @@ isDragging = false; element.classList.remove('dragging'); hideGuides(); + + // 保存状态 + saveState(); } // 阻止事件冒泡 @@ -1240,6 +1597,9 @@ editingInput = null; // 隐藏编辑器工具栏 editorToolbar.classList.remove('active'); + + // 保存状态 + saveState(); } // 更新工具栏状态 @@ -1735,6 +2095,7 @@ } boldBtn.classList.toggle('active'); } + saveState(); }); // 斜体按钮 @@ -1753,6 +2114,7 @@ } italicBtn.classList.toggle('active'); } + saveState(); }); // 锁定按钮 @@ -1767,6 +2129,7 @@ targetInput.classList.add('locked'); } lockBtn.classList.toggle('active', !isLocked); + saveState(); }); // 颜色选择器 @@ -1780,12 +2143,16 @@ targetInput.style.color = colorPicker.value; } }); + colorPicker.addEventListener('change', () => { + saveState(); + }); // 字体选择器 fontPicker.addEventListener('change', () => { const targetInput = editingInput || selectedInput; if (!targetInput) return; targetInput.style.fontFamily = fontPicker.value; + saveState(); }); // 字号选择器 - 密码锁式滚轮 @@ -1814,6 +2181,7 @@ if (targetInput) { const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; updateFontSize(currentSize + 1, true); + saveState(); } }); @@ -1822,6 +2190,7 @@ if (targetInput) { const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; updateFontSize(currentSize - 1, true); + saveState(); } }); @@ -1832,6 +2201,7 @@ const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; const delta = e.deltaY > 0 ? -1 : 1; updateFontSize(currentSize + delta, true); + saveState(); } }, { passive: false }); } @@ -1839,6 +2209,23 @@ // 初始化键盘微调功能(方向键 1px 移动) function initKeyboardNudge() { document.addEventListener('keydown', (e) => { + // 撤销重做快捷键 + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + e.preventDefault(); + if (e.shiftKey) { + redo(); + } else { + undo(); + } + return; + } + + if ((e.ctrlKey || e.metaKey) && e.key === 'y') { + e.preventDefault(); + redo(); + return; + } + // 如果正在编辑文本,不处理方向键 if (editingInput) return;