feat: 实现移动端长按保存图片,删除分享功能
This commit is contained in:
94
js/script.js
94
js/script.js
@@ -648,17 +648,101 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const canvas = await generateCanvas();
|
const canvas = await generateCanvas();
|
||||||
|
showMobileSaveTip();
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = '邀请函_' + Date.now() + '.png';
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
link.click();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存失败:', err);
|
console.error('保存失败:', err);
|
||||||
alert('保存失败,请重试');
|
alert('保存失败,请重试');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示移动端保存选项
|
||||||
|
function showMobileSaveTip() {
|
||||||
|
const canvas = exportCanvas;
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
const filename = '邀请函_' + Date.now() + '.png';
|
||||||
|
|
||||||
|
const tipOverlay = document.createElement('div');
|
||||||
|
tipOverlay.id = 'mobileSaveTip';
|
||||||
|
tipOverlay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const savePanel = document.createElement('div');
|
||||||
|
savePanel.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const title = document.createElement('p');
|
||||||
|
title.textContent = '保存图片';
|
||||||
|
title.style.cssText = `
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.textContent = '保存到相册';
|
||||||
|
saveBtn.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: #007AFF;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
`;
|
||||||
|
saveBtn.onclick = () => {
|
||||||
|
tipOverlay.remove();
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = filename;
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.textContent = '取消';
|
||||||
|
cancelBtn.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: #F2F2F7;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
cancelBtn.onclick = () => tipOverlay.remove();
|
||||||
|
|
||||||
|
tipOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === tipOverlay) {
|
||||||
|
tipOverlay.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
savePanel.appendChild(title);
|
||||||
|
savePanel.appendChild(saveBtn);
|
||||||
|
savePanel.appendChild(cancelBtn);
|
||||||
|
tipOverlay.appendChild(savePanel);
|
||||||
|
document.body.appendChild(tipOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
// 窗口大小改变时更新图片位置和边界
|
// 窗口大小改变时更新图片位置和边界
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
|
|||||||
523
script.js
523
script.js
@@ -1,523 +0,0 @@
|
|||||||
// 极简邀请函制作工具
|
|
||||||
(function() {
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
const addImageBtn = document.getElementById('addImageBtn');
|
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
|
||||||
const imageContainer = document.getElementById('imageContainer');
|
|
||||||
const bgImage = document.getElementById('bgImage');
|
|
||||||
const emptyTip = document.getElementById('emptyTip');
|
|
||||||
const input1 = document.getElementById('input1');
|
|
||||||
const input2 = document.getElementById('input2');
|
|
||||||
const exportCanvas = document.getElementById('exportCanvas');
|
|
||||||
const longPressTip = document.getElementById('longPressTip');
|
|
||||||
const canvasArea = document.getElementById('canvasArea');
|
|
||||||
const mobileInputControls = document.getElementById('mobileInputControls');
|
|
||||||
const mobileInput1 = document.getElementById('mobileInput1');
|
|
||||||
const mobileInput2 = document.getElementById('mobileInput2');
|
|
||||||
|
|
||||||
let currentImage = null;
|
|
||||||
let selectedInput = null;
|
|
||||||
let editingInput = null;
|
|
||||||
let longPressTimer = null;
|
|
||||||
let isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
init();
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
// 添加图片按钮
|
|
||||||
addImageBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 文件选择
|
|
||||||
fileInput.addEventListener('change', (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
loadImage(file);
|
|
||||||
}
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存按钮
|
|
||||||
saveBtn.addEventListener('click', saveImage);
|
|
||||||
|
|
||||||
// 初始化输入框
|
|
||||||
initInput(input1);
|
|
||||||
initInput(input2);
|
|
||||||
|
|
||||||
// 初始化手机端输入控制
|
|
||||||
initMobileInputControls();
|
|
||||||
|
|
||||||
// 点击外部取消选中
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('.text-input') &&
|
|
||||||
!e.target.closest('.image-container') &&
|
|
||||||
!e.target.closest('.mobile-input-controls')) {
|
|
||||||
deselectAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化长按保存
|
|
||||||
initLongPressSave();
|
|
||||||
|
|
||||||
// 初始化右键菜单
|
|
||||||
initContextMenu();
|
|
||||||
|
|
||||||
// 显示长按提示
|
|
||||||
setTimeout(() => {
|
|
||||||
longPressTip.classList.add('show');
|
|
||||||
setTimeout(() => {
|
|
||||||
longPressTip.classList.remove('show');
|
|
||||||
}, 3000);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// 监听屏幕尺寸变化
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化手机端输入控制
|
|
||||||
function initMobileInputControls() {
|
|
||||||
// 姓名输入同步
|
|
||||||
mobileInput1.addEventListener('input', () => {
|
|
||||||
input1.textContent = mobileInput1.value || '姓名';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 编号输入同步
|
|
||||||
mobileInput2.addEventListener('input', () => {
|
|
||||||
input2.textContent = mobileInput2.value || '编号';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 聚焦时选中对应的输入框
|
|
||||||
mobileInput1.addEventListener('focus', () => {
|
|
||||||
selectInput(input1);
|
|
||||||
});
|
|
||||||
|
|
||||||
mobileInput2.addEventListener('focus', () => {
|
|
||||||
selectInput(input2);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载图片
|
|
||||||
function loadImage(file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
currentImage = img;
|
|
||||||
bgImage.src = img.src;
|
|
||||||
imageContainer.classList.add('show');
|
|
||||||
emptyTip.classList.add('hidden');
|
|
||||||
mobileInputControls.classList.add('show');
|
|
||||||
setDefaultPositions();
|
|
||||||
};
|
|
||||||
img.src = e.target.result;
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认位置(左下角)
|
|
||||||
function setDefaultPositions() {
|
|
||||||
if (!currentImage) return;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const imgRect = bgImage.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 输入框1 - 左下角偏上
|
|
||||||
input1.style.left = '20px';
|
|
||||||
input1.style.top = (imgRect.height - 80) + 'px';
|
|
||||||
|
|
||||||
// 输入框2 - 左下角偏下
|
|
||||||
input2.style.left = '20px';
|
|
||||||
input2.style.top = (imgRect.height - 40) + 'px';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
|
|
||||||
function initInput(element) {
|
|
||||||
// 每个输入框独立的状态变量
|
|
||||||
let isMouseDown = false;
|
|
||||||
let isDragging = false;
|
|
||||||
let dragTimer = null;
|
|
||||||
let startX, startY, startLeft, startTop;
|
|
||||||
let clickStartTime = 0;
|
|
||||||
let clickStartX = 0;
|
|
||||||
let clickStartY = 0;
|
|
||||||
let hasMoved = false;
|
|
||||||
|
|
||||||
// 鼠标/触摸按下
|
|
||||||
element.addEventListener('mousedown', handleStart);
|
|
||||||
element.addEventListener('touchstart', handleStart, { passive: false });
|
|
||||||
|
|
||||||
function handleStart(e) {
|
|
||||||
const isTouch = e.type === 'touchstart';
|
|
||||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
|
||||||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
|
||||||
|
|
||||||
// 标记鼠标/手指按下
|
|
||||||
isMouseDown = true;
|
|
||||||
clickStartTime = Date.now();
|
|
||||||
clickStartX = clientX;
|
|
||||||
clickStartY = clientY;
|
|
||||||
hasMoved = false;
|
|
||||||
isDragging = false;
|
|
||||||
|
|
||||||
// 如果正在编辑此输入框,不处理
|
|
||||||
if (editingInput === element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中此输入框
|
|
||||||
selectInput(element);
|
|
||||||
|
|
||||||
startX = clientX;
|
|
||||||
startY = clientY;
|
|
||||||
startLeft = parseInt(element.style.left) || 0;
|
|
||||||
startTop = parseInt(element.style.top) || 0;
|
|
||||||
|
|
||||||
// 延迟判断是否为拖拽(150ms后判定为拖拽)
|
|
||||||
dragTimer = setTimeout(() => {
|
|
||||||
if (isMouseDown) {
|
|
||||||
isDragging = true;
|
|
||||||
element.classList.add('dragging');
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
if (isTouch) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标/触摸移动 - 绑定到元素上
|
|
||||||
element.addEventListener('mousemove', handleMove);
|
|
||||||
element.addEventListener('touchmove', handleMove, { passive: false });
|
|
||||||
|
|
||||||
function handleMove(e) {
|
|
||||||
// 必须按住鼠标/手指才能拖动
|
|
||||||
if (!isMouseDown) return;
|
|
||||||
if (!selectedInput || editingInput === element) return;
|
|
||||||
|
|
||||||
const isTouch = e.type === 'touchmove';
|
|
||||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
|
||||||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
|
||||||
|
|
||||||
// 判断是否移动
|
|
||||||
const moveDistance = Math.sqrt(
|
|
||||||
Math.pow(clientX - clickStartX, 2) +
|
|
||||||
Math.pow(clientY - clickStartY, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (moveDistance > 5) {
|
|
||||||
hasMoved = true;
|
|
||||||
if (dragTimer) {
|
|
||||||
clearTimeout(dragTimer);
|
|
||||||
dragTimer = null;
|
|
||||||
}
|
|
||||||
if (!isDragging) {
|
|
||||||
isDragging = true;
|
|
||||||
element.classList.add('dragging');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
const dx = clientX - startX;
|
|
||||||
const dy = clientY - startY;
|
|
||||||
element.style.left = (startLeft + dx) + 'px';
|
|
||||||
element.style.top = (startTop + dy) + 'px';
|
|
||||||
if (isTouch) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标/触摸释放 - 绑定到元素上
|
|
||||||
element.addEventListener('mouseup', handleEnd);
|
|
||||||
element.addEventListener('touchend', handleEnd);
|
|
||||||
|
|
||||||
// 鼠标离开元素时也结束拖拽
|
|
||||||
element.addEventListener('mouseleave', (e) => {
|
|
||||||
if (isMouseDown) {
|
|
||||||
handleEnd(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleEnd(e) {
|
|
||||||
// 标记鼠标/手指释放
|
|
||||||
isMouseDown = false;
|
|
||||||
|
|
||||||
// 清除拖拽定时器
|
|
||||||
if (dragTimer) {
|
|
||||||
clearTimeout(dragTimer);
|
|
||||||
dragTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickDuration = Date.now() - clickStartTime;
|
|
||||||
|
|
||||||
// 如果是快速点击且未移动,判定为单击(仅选中,不编辑)
|
|
||||||
if (!hasMoved && clickDuration < 200 && !isDragging) {
|
|
||||||
// 单击仅选中,不进入编辑模式
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即结束拖拽状态
|
|
||||||
if (isDragging) {
|
|
||||||
isDragging = false;
|
|
||||||
element.classList.remove('dragging');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 双击进入编辑模式
|
|
||||||
element.addEventListener('dblclick', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
startEdit(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 触摸双击检测
|
|
||||||
let lastTapTime = 0;
|
|
||||||
element.addEventListener('touchend', (e) => {
|
|
||||||
const currentTime = Date.now();
|
|
||||||
if (currentTime - lastTapTime < 300) {
|
|
||||||
// 双击检测
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
startEdit(element);
|
|
||||||
}
|
|
||||||
lastTapTime = currentTime;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中输入框
|
|
||||||
function selectInput(element) {
|
|
||||||
// 如果有其他输入框在编辑,先退出编辑
|
|
||||||
if (editingInput && editingInput !== element) {
|
|
||||||
endEdit(editingInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消之前的选中
|
|
||||||
if (selectedInput && selectedInput !== element) {
|
|
||||||
selectedInput.classList.remove('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中当前输入框
|
|
||||||
selectedInput = element;
|
|
||||||
element.classList.add('selected');
|
|
||||||
element.classList.remove('editing');
|
|
||||||
element.contentEditable = 'false';
|
|
||||||
|
|
||||||
// 同步手机端输入框的值
|
|
||||||
if (element === input1) {
|
|
||||||
mobileInput1.value = element.textContent === '姓名' ? '' : element.textContent;
|
|
||||||
} else if (element === input2) {
|
|
||||||
mobileInput2.value = element.textContent === '编号' ? '' : element.textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始编辑
|
|
||||||
function startEdit(element) {
|
|
||||||
if (editingInput && editingInput !== element) {
|
|
||||||
endEdit(editingInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
editingInput = element;
|
|
||||||
selectedInput = element;
|
|
||||||
|
|
||||||
element.classList.add('editing');
|
|
||||||
element.classList.remove('selected');
|
|
||||||
element.contentEditable = 'true';
|
|
||||||
element.focus();
|
|
||||||
|
|
||||||
// 光标定位到末尾
|
|
||||||
const range = document.createRange();
|
|
||||||
const sel = window.getSelection();
|
|
||||||
range.selectNodeContents(element);
|
|
||||||
range.collapse(false);
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
|
|
||||||
// 移动端显示键盘
|
|
||||||
if ('virtualKeyboard' in navigator) {
|
|
||||||
navigator.virtualKeyboard.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 结束编辑
|
|
||||||
function endEdit(element) {
|
|
||||||
element.classList.remove('editing');
|
|
||||||
element.contentEditable = 'false';
|
|
||||||
element.blur();
|
|
||||||
editingInput = null;
|
|
||||||
|
|
||||||
// 同步到手机端输入框
|
|
||||||
if (element === input1) {
|
|
||||||
mobileInput1.value = element.textContent === '姓名' ? '' : element.textContent;
|
|
||||||
} else if (element === input2) {
|
|
||||||
mobileInput2.value = element.textContent === '编号' ? '' : element.textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消所有选中
|
|
||||||
function deselectAll() {
|
|
||||||
if (editingInput) {
|
|
||||||
endEdit(editingInput);
|
|
||||||
}
|
|
||||||
if (selectedInput) {
|
|
||||||
selectedInput.classList.remove('selected');
|
|
||||||
selectedInput = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化长按保存
|
|
||||||
function initLongPressSave() {
|
|
||||||
canvasArea.addEventListener('touchstart', (e) => {
|
|
||||||
// 如果触摸的是输入框或手机端控制区,不触发长按保存
|
|
||||||
if (e.target.closest('.text-input') || e.target.closest('.mobile-input-controls')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
longPressTimer = setTimeout(() => {
|
|
||||||
// 震动反馈
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
navigator.vibrate(50);
|
|
||||||
}
|
|
||||||
// 保存图片
|
|
||||||
saveImage();
|
|
||||||
}, 800);
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
canvasArea.addEventListener('touchend', () => {
|
|
||||||
if (longPressTimer) {
|
|
||||||
clearTimeout(longPressTimer);
|
|
||||||
longPressTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
canvasArea.addEventListener('touchmove', () => {
|
|
||||||
if (longPressTimer) {
|
|
||||||
clearTimeout(longPressTimer);
|
|
||||||
longPressTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化右键菜单
|
|
||||||
function initContextMenu() {
|
|
||||||
// 禁用默认右键菜单
|
|
||||||
canvasArea.addEventListener('contextmenu', (e) => {
|
|
||||||
// 如果右键点击的是输入框,显示默认菜单
|
|
||||||
if (e.target.closest('.text-input')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
showContextMenu(e.clientX, e.clientY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 点击其他地方关闭菜单
|
|
||||||
document.addEventListener('click', () => {
|
|
||||||
hideContextMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示右键菜单
|
|
||||||
function showContextMenu(x, y) {
|
|
||||||
hideContextMenu();
|
|
||||||
|
|
||||||
const menu = document.createElement('div');
|
|
||||||
menu.className = 'context-menu';
|
|
||||||
menu.id = 'contextMenu';
|
|
||||||
menu.style.left = x + 'px';
|
|
||||||
menu.style.top = y + 'px';
|
|
||||||
|
|
||||||
const saveItem = document.createElement('div');
|
|
||||||
saveItem.className = 'context-menu-item';
|
|
||||||
saveItem.textContent = '保存图片';
|
|
||||||
saveItem.addEventListener('click', () => {
|
|
||||||
saveImage();
|
|
||||||
hideContextMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.appendChild(saveItem);
|
|
||||||
document.body.appendChild(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏右键菜单
|
|
||||||
function hideContextMenu() {
|
|
||||||
const existingMenu = document.getElementById('contextMenu');
|
|
||||||
if (existingMenu) {
|
|
||||||
existingMenu.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存图片
|
|
||||||
function saveImage() {
|
|
||||||
if (!currentImage) {
|
|
||||||
alert('请先添加背景图片');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消所有选中和编辑状态
|
|
||||||
deselectAll();
|
|
||||||
|
|
||||||
const canvas = exportCanvas;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// 设置画布尺寸为图片原始尺寸
|
|
||||||
canvas.width = currentImage.naturalWidth;
|
|
||||||
canvas.height = currentImage.naturalHeight;
|
|
||||||
|
|
||||||
// 绘制背景图
|
|
||||||
ctx.drawImage(currentImage, 0, 0);
|
|
||||||
|
|
||||||
// 计算缩放比例
|
|
||||||
const scaleX = currentImage.naturalWidth / bgImage.clientWidth;
|
|
||||||
const scaleY = currentImage.naturalHeight / bgImage.clientHeight;
|
|
||||||
|
|
||||||
// 绘制文字
|
|
||||||
drawTextOnCanvas(ctx, input1, scaleX, scaleY);
|
|
||||||
drawTextOnCanvas(ctx, input2, scaleX, scaleY);
|
|
||||||
|
|
||||||
// 下载图片
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = '邀请函_' + Date.now() + '.png';
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在画布上绘制文字
|
|
||||||
function drawTextOnCanvas(ctx, inputElement, scaleX, scaleY) {
|
|
||||||
const text = inputElement.textContent;
|
|
||||||
const style = window.getComputedStyle(inputElement);
|
|
||||||
|
|
||||||
// 获取输入框位置
|
|
||||||
const inputRect = inputElement.getBoundingClientRect();
|
|
||||||
const imgRect = bgImage.getBoundingClientRect();
|
|
||||||
|
|
||||||
const relativeX = inputRect.left - imgRect.left;
|
|
||||||
const relativeY = inputRect.top - imgRect.top;
|
|
||||||
|
|
||||||
// 转换为画布坐标
|
|
||||||
const canvasX = relativeX * scaleX;
|
|
||||||
const canvasY = relativeY * scaleY;
|
|
||||||
|
|
||||||
// 设置字体样式
|
|
||||||
const fontSize = parseInt(style.fontSize) * scaleX;
|
|
||||||
ctx.font = `${fontSize}px "${style.fontFamily}"`;
|
|
||||||
ctx.fillStyle = style.color;
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
|
|
||||||
// 绘制文字背景
|
|
||||||
const padding = 8 * scaleX;
|
|
||||||
const textMetrics = ctx.measureText(text);
|
|
||||||
const textWidth = textMetrics.width;
|
|
||||||
const textHeight = fontSize * 1.4;
|
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
|
||||||
ctx.fillRect(canvasX - padding, canvasY - padding, textWidth + padding * 2, textHeight + padding);
|
|
||||||
|
|
||||||
// 绘制文字
|
|
||||||
ctx.fillStyle = style.color;
|
|
||||||
ctx.fillText(text, canvasX, canvasY);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
28
server.js
28
server.js
@@ -1,28 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
const filePath = req.url === '/' ? 'index.html' : req.url.slice(1);
|
|
||||||
const ext = path.extname(filePath);
|
|
||||||
const mimeTypes = {
|
|
||||||
'.html': 'text/html',
|
|
||||||
'.js': 'application/javascript',
|
|
||||||
'.css': 'text/css'
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.readFile(path.join(process.cwd(), filePath), (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
res.writeHead(404);
|
|
||||||
res.end('Not Found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
|
|
||||||
res.end(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(0, () => {
|
|
||||||
const port = server.address().port;
|
|
||||||
console.log('Server running on http://localhost:' + port);
|
|
||||||
});
|
|
||||||
238
style.css
238
style.css
@@ -1,238 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
||||||
background: #f5f5f5;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部工具栏 */
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 10px 24px;
|
|
||||||
font-size: 15px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #4a90e2;
|
|
||||||
color: #fff;
|
|
||||||
border-color: #4a90e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
background: #3a7bc8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图片展示区 */
|
|
||||||
.canvas-area {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-tip {
|
|
||||||
color: #999;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bgImage {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: calc(100vh - 200px);
|
|
||||||
object-fit: contain;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文字输入框 */
|
|
||||||
.text-input {
|
|
||||||
position: absolute;
|
|
||||||
min-width: 80px;
|
|
||||||
min-height: 32px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #333;
|
|
||||||
background: rgba(255,255,255,0.9);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
cursor: grab;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1.4;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选中状态(单击) */
|
|
||||||
.text-input.selected {
|
|
||||||
border-color: #4a90e2;
|
|
||||||
box-shadow: 0 0 0 2px rgba(74,144,226,0.3);
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 编辑状态(双击) */
|
|
||||||
.text-input.editing {
|
|
||||||
border-color: #4a90e2;
|
|
||||||
background: rgba(255,255,255,0.95);
|
|
||||||
cursor: text;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽状态 */
|
|
||||||
.text-input.dragging {
|
|
||||||
opacity: 0.8;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏空提示 */
|
|
||||||
.empty-tip.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端输入控制区 */
|
|
||||||
.mobile-input-controls {
|
|
||||||
display: none;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
gap: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-input-controls.show {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-control-group {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-control-group label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-control-group input {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-control-group input:focus {
|
|
||||||
border-color: #4a90e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 长按提示 */
|
|
||||||
.long-press-tip {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: rgba(0,0,0,0.7);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.long-press-tip.show {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 右键菜单 */
|
|
||||||
.context-menu {
|
|
||||||
position: fixed;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
||||||
padding: 4px 0;
|
|
||||||
z-index: 1000;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item {
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item:active {
|
|
||||||
background: #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端适配 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mobile-input-controls {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bgImage {
|
|
||||||
max-height: calc(100vh - 280px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user