feat(editor): 添加撤销重做功能

实现完整的撤销重做功能,包括:
1. 添加工具栏撤销/重做按钮及快捷键支持
2. 实现状态保存与恢复机制
3. 添加状态变化检测与历史记录管理
4. 更新相关UI交互以触发状态保存
This commit is contained in:
Zhukj
2026-04-22 14:08:42 +08:00
parent e46805deca
commit 1983c15fb9
3 changed files with 428 additions and 0 deletions

View File

@@ -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;