fix: NEW修复预览与保存不一致,添加3:4比例,输入框透明无边框
This commit is contained in:
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
***项目要求***
|
||||||
|
一款以手机端为主、电脑端兼容的简易邀请函在线制作工具,无需安装、无需后端,打开浏览器即可使用。用户可上传背景图、添加文字、拖动位置,最终一键生成并保存完整邀请函图片。
|
||||||
|
核心功能
|
||||||
|
1. 设备适配
|
||||||
|
移动端优先设计,手机操作最流畅
|
||||||
|
电脑端完全兼容,可正常制作、编辑、保存
|
||||||
|
背景图自动适应屏幕大小,不变形、不拉伸
|
||||||
|
2. 预览区域规范
|
||||||
|
预览区域固定 3:5 比例
|
||||||
|
容器居中显示,美观大方
|
||||||
|
背景图自适应铺满容器
|
||||||
|
3. 背景图上传
|
||||||
|
支持本地图片上传
|
||||||
|
支持 jpg /png/jpeg 格式
|
||||||
|
自动居中、自适应保留原有比例
|
||||||
|
4. 输入框功能
|
||||||
|
页面加载自动在右下角生成 2 个输入框,输入框为透明无边框设计
|
||||||
|
双击输入框 → 编辑文字
|
||||||
|
点击 / 长按输入框 → 可拖动位置
|
||||||
|
两个输入框完全独立,拖动互不影响
|
||||||
|
可随意拖拽到预览区域内任意位置
|
||||||
|
5. 操作交互优化
|
||||||
|
点击空白区域 → 自动取消输入框选中状态
|
||||||
|
未选中时无法编辑、无法拖动
|
||||||
|
避免误触、避免乱跑、避免重叠操作
|
||||||
|
6. 图片保存 / 导出(最重要)
|
||||||
|
电脑端:右键预览区域 → 自动合成并保存图片
|
||||||
|
手机端:长按预览区域 → 自动合成并保存图片
|
||||||
|
合成内容 = 背景图 + 所有输入框文字
|
||||||
|
输出一张完整的邀请函图片
|
||||||
|
***使用说明(最简流程)***
|
||||||
|
打开页面
|
||||||
|
上传背景图
|
||||||
|
双击输入框填写文字
|
||||||
|
单击输入框拖动到合适位置
|
||||||
|
电脑:右键保存图片
|
||||||
|
手机:长按保存图片
|
||||||
|
完成制作
|
||||||
|
|
||||||
|
## 要求记录
|
||||||
|
1. 我要求打开项目即显示 3:5 空白面板,背景图自动覆盖容器、超出部分自动裁剪,不拉伸、不变形
|
||||||
|
2. 我要求两个独立输入框,可双击编辑、可拖动,文字背景透明无边框
|
||||||
|
3. 我要求电脑端:右键预览区域可直接复制图片到剪贴板,移动端:长按预览区域可保存图片,操作简单,一键导出图片(复制 / 保存)
|
||||||
|
4. 预览的图片样式和保存的样式不一样,需要改成预览是什么样子,保存出来的图片内容就是什么样子
|
||||||
198
css/style.css
Normal file
198
css/style.css
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区域固定 3:5 比例,打开即显示空白面板 */
|
||||||
|
.image-container {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
aspect-ratio: 3 / 5;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container.empty {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景图自适应铺满容器,超出部分裁剪 */
|
||||||
|
#bgImage {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bgImage[src=""] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字输入框 - 透明无边框设计 */
|
||||||
|
.text-input {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 80px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
background: transparent;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态(单击/长按) - 仅显示虚线边框 */
|
||||||
|
.text-input.selected {
|
||||||
|
border: 2px dashed rgba(74, 144, 226, 0.6);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑状态(双击) - 显示实线边框 */
|
||||||
|
.text-input.editing {
|
||||||
|
border: 2px solid #4a90e2;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
cursor: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽状态 */
|
||||||
|
.text-input.dragging {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏空提示 */
|
||||||
|
.empty-tip.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 长按提示 */
|
||||||
|
.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: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
22
index.html
22
index.html
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>极简邀请函制作</title>
|
<title>极简邀请函制作</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -17,24 +17,12 @@
|
|||||||
<!-- 图片展示区 -->
|
<!-- 图片展示区 -->
|
||||||
<div class="canvas-area" id="canvasArea">
|
<div class="canvas-area" id="canvasArea">
|
||||||
<div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div>
|
<div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div>
|
||||||
<div class="image-container" id="imageContainer">
|
<div class="image-container empty" id="imageContainer">
|
||||||
<img id="bgImage" src="" alt="背景图">
|
<img id="bgImage" src="" alt="背景图">
|
||||||
<!-- 文字输入框1 -->
|
<!-- 文字输入框1 -->
|
||||||
<div class="text-input" id="input1" contenteditable="false">姓名:</div>
|
<div class="text-input" id="input1" contenteditable="false" style="left: 60%; top: 75%;">姓名</div>
|
||||||
<!-- 文字输入框2 -->
|
<!-- 文字输入框2 -->
|
||||||
<div class="text-input" id="input2" contenteditable="false">编号:</div>
|
<div class="text-input" id="input2" contenteditable="false" style="left: 60%; top: 85%;">编号</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 手机端输入控制区 -->
|
|
||||||
<div class="mobile-input-controls" id="mobileInputControls">
|
|
||||||
<div class="input-control-group">
|
|
||||||
<label>姓名:</label>
|
|
||||||
<input type="text" id="mobileInput1" placeholder="请输入姓名" value="姓名">
|
|
||||||
</div>
|
|
||||||
<div class="input-control-group">
|
|
||||||
<label>编号:</label>
|
|
||||||
<input type="text" id="mobileInput2" placeholder="请输入编号" value="编号">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,6 +33,6 @@
|
|||||||
<input type="file" id="fileInput" accept="image/*" hidden>
|
<input type="file" id="fileInput" accept="image/*" hidden>
|
||||||
<canvas id="exportCanvas" hidden></canvas>
|
<canvas id="exportCanvas" hidden></canvas>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="js/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
516
js/script.js
Normal file
516
js/script.js
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
// 极简邀请函制作工具
|
||||||
|
(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');
|
||||||
|
|
||||||
|
let currentImage = null;
|
||||||
|
let selectedInput = null;
|
||||||
|
let editingInput = null;
|
||||||
|
let longPressTimer = null;
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 点击外部取消选中
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.text-input') && !e.target.closest('.image-container')) {
|
||||||
|
deselectAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化长按保存
|
||||||
|
initLongPressSave();
|
||||||
|
|
||||||
|
// 初始化右键菜单
|
||||||
|
initContextMenu();
|
||||||
|
|
||||||
|
// 显示长按提示
|
||||||
|
setTimeout(() => {
|
||||||
|
longPressTip.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
longPressTip.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载图片
|
||||||
|
function loadImage(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
currentImage = img;
|
||||||
|
bgImage.src = img.src;
|
||||||
|
imageContainer.classList.remove('empty');
|
||||||
|
emptyTip.classList.add('hidden');
|
||||||
|
};
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
|
||||||
|
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 = element.offsetLeft;
|
||||||
|
startTop = element.offsetTop;
|
||||||
|
|
||||||
|
// 延迟判断是否为拖拽(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) {
|
||||||
|
// 单击仅选中,不进入编辑模式
|
||||||
|
if (e && e.preventDefault) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始编辑
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消所有选中
|
||||||
|
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')) {
|
||||||
|
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 copyItem = document.createElement('div');
|
||||||
|
copyItem.className = 'context-menu-item';
|
||||||
|
copyItem.textContent = '复制图片';
|
||||||
|
copyItem.addEventListener('click', async () => {
|
||||||
|
await copyImageToClipboard();
|
||||||
|
hideContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存图片选项
|
||||||
|
const saveItem = document.createElement('div');
|
||||||
|
saveItem.className = 'context-menu-item';
|
||||||
|
saveItem.textContent = '保存图片';
|
||||||
|
saveItem.addEventListener('click', () => {
|
||||||
|
saveImage();
|
||||||
|
hideContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.appendChild(copyItem);
|
||||||
|
menu.appendChild(saveItem);
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏右键菜单
|
||||||
|
function hideContextMenu() {
|
||||||
|
const existingMenu = document.getElementById('contextMenu');
|
||||||
|
if (existingMenu) {
|
||||||
|
existingMenu.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制图片到剪贴板
|
||||||
|
async function copyImageToClipboard() {
|
||||||
|
if (!currentImage) {
|
||||||
|
alert('请先添加背景图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 取消所有选中和编辑状态
|
||||||
|
deselectAll();
|
||||||
|
|
||||||
|
const canvas = await generateCanvas();
|
||||||
|
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||||
|
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ 'image/png': blob })
|
||||||
|
]);
|
||||||
|
|
||||||
|
alert('图片已复制到剪贴板');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
alert('复制失败,请尝试保存图片');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成画布(预览与保存样式一致)
|
||||||
|
async function generateCanvas() {
|
||||||
|
const canvas = exportCanvas;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// 获取容器实际尺寸
|
||||||
|
const containerWidth = imageContainer.clientWidth;
|
||||||
|
const containerHeight = imageContainer.clientHeight;
|
||||||
|
|
||||||
|
// 设置画布尺寸与容器一致
|
||||||
|
canvas.width = containerWidth;
|
||||||
|
canvas.height = containerHeight;
|
||||||
|
|
||||||
|
// 绘制白色背景
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 如果有背景图,绘制背景图(保持cover效果)
|
||||||
|
if (currentImage) {
|
||||||
|
drawImageCover(ctx, currentImage, 0, 0, containerWidth, containerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制文字(保持与预览完全一致的位置和样式)
|
||||||
|
drawTextOnCanvas(ctx, input1, containerWidth, containerHeight);
|
||||||
|
drawTextOnCanvas(ctx, input2, containerWidth, containerHeight);
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制图片保持cover效果(与CSS object-fit: cover一致)
|
||||||
|
function drawImageCover(ctx, img, x, y, width, height) {
|
||||||
|
const imgRatio = img.naturalWidth / img.naturalHeight;
|
||||||
|
const containerRatio = width / height;
|
||||||
|
|
||||||
|
let drawWidth, drawHeight, drawX, drawY;
|
||||||
|
|
||||||
|
if (imgRatio > containerRatio) {
|
||||||
|
// 图片更宽,以高度为基准
|
||||||
|
drawHeight = height;
|
||||||
|
drawWidth = height * imgRatio;
|
||||||
|
drawX = x + (width - drawWidth) / 2;
|
||||||
|
drawY = y;
|
||||||
|
} else {
|
||||||
|
// 图片更高,以宽度为基准
|
||||||
|
drawWidth = width;
|
||||||
|
drawHeight = width / imgRatio;
|
||||||
|
drawX = x;
|
||||||
|
drawY = y + (height - drawHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在画布上绘制文字(保持与预览完全一致)
|
||||||
|
function drawTextOnCanvas(ctx, inputElement, containerWidth, containerHeight) {
|
||||||
|
const text = inputElement.textContent;
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(inputElement);
|
||||||
|
|
||||||
|
// 获取输入框位置(相对于容器)
|
||||||
|
const inputLeft = inputElement.offsetLeft;
|
||||||
|
const inputTop = inputElement.offsetTop;
|
||||||
|
|
||||||
|
// 设置字体样式(与预览完全一致)
|
||||||
|
const fontSize = parseInt(style.fontSize);
|
||||||
|
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
|
||||||
|
ctx.fillStyle = style.color;
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
// 绘制文字(保持与预览完全一致的位置)
|
||||||
|
ctx.fillText(text, inputLeft, inputTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存图片
|
||||||
|
async function saveImage() {
|
||||||
|
if (!currentImage) {
|
||||||
|
alert('请先添加背景图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvas = await generateCanvas();
|
||||||
|
|
||||||
|
// 下载图片
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = '邀请函_' + Date.now() + '.png';
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存失败:', err);
|
||||||
|
alert('保存失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user