feat(editor): 添加撤销重做功能
实现完整的撤销重做功能,包括: 1. 添加工具栏撤销/重做按钮及快捷键支持 2. 实现状态保存与恢复机制 3. 添加状态变化检测与历史记录管理 4. 更新相关UI交互以触发状态保存
This commit is contained in:
@@ -62,6 +62,43 @@ html, body {
|
|||||||
background: #3a7bc8;
|
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 {
|
.text-input .text-delete-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
<button class="btn" id="addImageBtn">背景图</button>
|
<button class="btn" id="addImageBtn">背景图</button>
|
||||||
<button class="btn" id="addOverlayBtn">层叠图</button>
|
<button class="btn" id="addOverlayBtn">层叠图</button>
|
||||||
<button class="btn" id="addTextBtn">文本</button>
|
<button class="btn" id="addTextBtn">文本</button>
|
||||||
|
<div class="toolbar-divider-v"></div>
|
||||||
|
<button class="toolbar-btn-icon" id="undoBtn" title="撤销 (Ctrl+Z)">↩</button>
|
||||||
|
<button class="toolbar-btn-icon" id="redoBtn" title="重做 (Ctrl+Y)">↪</button>
|
||||||
|
<div class="toolbar-divider-v"></div>
|
||||||
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
|
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
387
js/script.js
387
js/script.js
@@ -60,6 +60,15 @@
|
|||||||
let maxOffsetX = 0;
|
let maxOffsetX = 0;
|
||||||
let isInteractingWithBgImage = false;
|
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();
|
init();
|
||||||
|
|
||||||
@@ -100,6 +109,23 @@
|
|||||||
// 保存按钮
|
// 保存按钮
|
||||||
saveBtn.addEventListener('click', saveImage);
|
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(input1);
|
||||||
initInput(input2);
|
initInput(input2);
|
||||||
@@ -127,6 +153,9 @@
|
|||||||
opacityValue.textContent = e.target.value + '%';
|
opacityValue.textContent = e.target.value + '%';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
opacitySlider.addEventListener('change', () => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// 旋转滑块
|
// 旋转滑块
|
||||||
rotateSlider.addEventListener('input', (e) => {
|
rotateSlider.addEventListener('input', (e) => {
|
||||||
@@ -150,6 +179,9 @@
|
|||||||
rotateValue.textContent = rotateDeg + '°';
|
rotateValue.textContent = rotateDeg + '°';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
rotateSlider.addEventListener('change', () => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// 水平镜像按钮
|
// 水平镜像按钮
|
||||||
mirrorHBtn.addEventListener('click', () => {
|
mirrorHBtn.addEventListener('click', () => {
|
||||||
@@ -171,6 +203,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img.style.transform = newTransform;
|
img.style.transform = newTransform;
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -181,6 +216,9 @@
|
|||||||
bgScaleValue.textContent = e.target.value + '%';
|
bgScaleValue.textContent = e.target.value + '%';
|
||||||
updateBgImage();
|
updateBgImage();
|
||||||
});
|
});
|
||||||
|
bgScaleSlider.addEventListener('change', () => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// 背景图透明度滑块
|
// 背景图透明度滑块
|
||||||
bgOpacitySlider.addEventListener('input', (e) => {
|
bgOpacitySlider.addEventListener('input', (e) => {
|
||||||
@@ -188,6 +226,9 @@
|
|||||||
bgOpacityValue.textContent = e.target.value + '%';
|
bgOpacityValue.textContent = e.target.value + '%';
|
||||||
bgImage.style.opacity = bgImageOpacity;
|
bgImage.style.opacity = bgImageOpacity;
|
||||||
});
|
});
|
||||||
|
bgOpacitySlider.addEventListener('change', () => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// 点击空白区域取消选中
|
// 点击空白区域取消选中
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
@@ -273,9 +314,301 @@
|
|||||||
// 加载默认背景图
|
// 加载默认背景图
|
||||||
loadDefaultBackground();
|
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() {
|
function loadDefaultBackground() {
|
||||||
const defaultBgUrl = './images/default-bg.jpg';
|
const defaultBgUrl = './images/default-bg.jpg';
|
||||||
@@ -334,6 +667,9 @@
|
|||||||
|
|
||||||
// 显示预设的文本输入框
|
// 显示预设的文本输入框
|
||||||
showPresetTextInputs();
|
showPresetTextInputs();
|
||||||
|
|
||||||
|
// 保存状态(立即)
|
||||||
|
saveState(true);
|
||||||
};
|
};
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
};
|
};
|
||||||
@@ -381,6 +717,9 @@
|
|||||||
initInput(textEl);
|
initInput(textEl);
|
||||||
|
|
||||||
selectInput(textEl);
|
selectInput(textEl);
|
||||||
|
|
||||||
|
// 保存状态(立即)
|
||||||
|
saveState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载层叠图
|
// 加载层叠图
|
||||||
@@ -436,6 +775,8 @@
|
|||||||
deleteBtn.addEventListener('click', (e) => {
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
overlayEl.remove();
|
overlayEl.remove();
|
||||||
|
// 保存状态(立即)
|
||||||
|
saveState(true);
|
||||||
});
|
});
|
||||||
overlayEl.appendChild(deleteBtn);
|
overlayEl.appendChild(deleteBtn);
|
||||||
|
|
||||||
@@ -452,6 +793,9 @@
|
|||||||
|
|
||||||
// 选中新添加的层叠图
|
// 选中新添加的层叠图
|
||||||
selectOverlay(overlayEl);
|
selectOverlay(overlayEl);
|
||||||
|
|
||||||
|
// 保存状态(立即)
|
||||||
|
saveState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化层叠图交互
|
// 初始化层叠图交互
|
||||||
@@ -534,6 +878,9 @@
|
|||||||
document.removeEventListener('mouseup', handleDragEnd);
|
document.removeEventListener('mouseup', handleDragEnd);
|
||||||
document.removeEventListener('touchmove', handleDragMove);
|
document.removeEventListener('touchmove', handleDragMove);
|
||||||
document.removeEventListener('touchend', handleDragEnd);
|
document.removeEventListener('touchend', handleDragEnd);
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缩放控制(四角)
|
// 缩放控制(四角)
|
||||||
@@ -633,6 +980,9 @@
|
|||||||
document.removeEventListener('mouseup', endResize);
|
document.removeEventListener('mouseup', endResize);
|
||||||
document.removeEventListener('touchmove', handleResize);
|
document.removeEventListener('touchmove', handleResize);
|
||||||
document.removeEventListener('touchend', endResize);
|
document.removeEventListener('touchend', endResize);
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -935,6 +1285,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleImageEnd() {
|
function handleImageEnd() {
|
||||||
|
if (isDraggingImage) {
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
isDraggingImage = false;
|
isDraggingImage = false;
|
||||||
isInteractingWithBgImage = false;
|
isInteractingWithBgImage = false;
|
||||||
}
|
}
|
||||||
@@ -1098,6 +1452,9 @@
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
element.classList.remove('dragging');
|
element.classList.remove('dragging');
|
||||||
hideGuides();
|
hideGuides();
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阻止事件冒泡
|
// 阻止事件冒泡
|
||||||
@@ -1240,6 +1597,9 @@
|
|||||||
editingInput = null;
|
editingInput = null;
|
||||||
// 隐藏编辑器工具栏
|
// 隐藏编辑器工具栏
|
||||||
editorToolbar.classList.remove('active');
|
editorToolbar.classList.remove('active');
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新工具栏状态
|
// 更新工具栏状态
|
||||||
@@ -1735,6 +2095,7 @@
|
|||||||
}
|
}
|
||||||
boldBtn.classList.toggle('active');
|
boldBtn.classList.toggle('active');
|
||||||
}
|
}
|
||||||
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 斜体按钮
|
// 斜体按钮
|
||||||
@@ -1753,6 +2114,7 @@
|
|||||||
}
|
}
|
||||||
italicBtn.classList.toggle('active');
|
italicBtn.classList.toggle('active');
|
||||||
}
|
}
|
||||||
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 锁定按钮
|
// 锁定按钮
|
||||||
@@ -1767,6 +2129,7 @@
|
|||||||
targetInput.classList.add('locked');
|
targetInput.classList.add('locked');
|
||||||
}
|
}
|
||||||
lockBtn.classList.toggle('active', !isLocked);
|
lockBtn.classList.toggle('active', !isLocked);
|
||||||
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 颜色选择器
|
// 颜色选择器
|
||||||
@@ -1780,12 +2143,16 @@
|
|||||||
targetInput.style.color = colorPicker.value;
|
targetInput.style.color = colorPicker.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
colorPicker.addEventListener('change', () => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// 字体选择器
|
// 字体选择器
|
||||||
fontPicker.addEventListener('change', () => {
|
fontPicker.addEventListener('change', () => {
|
||||||
const targetInput = editingInput || selectedInput;
|
const targetInput = editingInput || selectedInput;
|
||||||
if (!targetInput) return;
|
if (!targetInput) return;
|
||||||
targetInput.style.fontFamily = fontPicker.value;
|
targetInput.style.fontFamily = fontPicker.value;
|
||||||
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 字号选择器 - 密码锁式滚轮
|
// 字号选择器 - 密码锁式滚轮
|
||||||
@@ -1814,6 +2181,7 @@
|
|||||||
if (targetInput) {
|
if (targetInput) {
|
||||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||||
updateFontSize(currentSize + 1, true);
|
updateFontSize(currentSize + 1, true);
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1822,6 +2190,7 @@
|
|||||||
if (targetInput) {
|
if (targetInput) {
|
||||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||||
updateFontSize(currentSize - 1, true);
|
updateFontSize(currentSize - 1, true);
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1832,6 +2201,7 @@
|
|||||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||||
const delta = e.deltaY > 0 ? -1 : 1;
|
const delta = e.deltaY > 0 ? -1 : 1;
|
||||||
updateFontSize(currentSize + delta, true);
|
updateFontSize(currentSize + delta, true);
|
||||||
|
saveState();
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
@@ -1839,6 +2209,23 @@
|
|||||||
// 初始化键盘微调功能(方向键 1px 移动)
|
// 初始化键盘微调功能(方向键 1px 移动)
|
||||||
function initKeyboardNudge() {
|
function initKeyboardNudge() {
|
||||||
document.addEventListener('keydown', (e) => {
|
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;
|
if (editingInput) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user