From ba45fd05928a380d06f6ee135e3ef80f154cba44 Mon Sep 17 00:00:00 2001 From: Zhukj <3262118957> Date: Tue, 14 Apr 2026 18:00:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E6=9D=BE=E5=BC=80=E9=BC=A0=E6=A0=87?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E6=8B=96=E5=8A=A8=EF=BC=8C=E4=BA=92=E4=B8=8D?= =?UTF-8?q?=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 50 +++++ script.js | 523 +++++++++++++++++++++++++++++++++++++++++++++++++++++ server.js | 28 +++ style.css | 238 ++++++++++++++++++++++++ 4 files changed, 839 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 server.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..56332ed --- /dev/null +++ b/index.html @@ -0,0 +1,50 @@ + + + + + + 极简邀请函制作 + + + +
+ +
+ + +
+ + +
+
请点击上方「添加图片」按钮上传背景图
+
+ 背景图 + +
姓名:
+ +
编号:
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
长按屏幕可保存图片
+
+ + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..72ae583 --- /dev/null +++ b/script.js @@ -0,0 +1,523 @@ +// 极简邀请函制作工具 +(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 new file mode 100644 index 0000000..1f7fc7e --- /dev/null +++ b/server.js @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..8d0e732 --- /dev/null +++ b/style.css @@ -0,0 +1,238 @@ +* { + 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); + } +}