diff --git a/js/script.js b/js/script.js index dbec769..e482900 100644 --- a/js/script.js +++ b/js/script.js @@ -648,17 +648,101 @@ try { const canvas = await generateCanvas(); - - const link = document.createElement('a'); - link.download = '邀请函_' + Date.now() + '.png'; - link.href = canvas.toDataURL('image/png'); - link.click(); + showMobileSaveTip(); } catch (err) { console.error('保存失败:', err); 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', () => { if (currentImage) { diff --git a/script.js b/script.js deleted file mode 100644 index 72ae583..0000000 --- a/script.js +++ /dev/null @@ -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); - } -})(); diff --git a/server.js b/server.js deleted file mode 100644 index 1f7fc7e..0000000 --- a/server.js +++ /dev/null @@ -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); -}); diff --git a/style.css b/style.css deleted file mode 100644 index 8d0e732..0000000 --- a/style.css +++ /dev/null @@ -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); - } -}