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);
+ }
+}