fix(editor): 修复字号读取及工具栏逻辑

重构编辑器工具栏字号调整逻辑,优先读取内联样式字号,
修复字号显示不同步问题,并更新 README 文档
This commit is contained in:
2026-05-15 14:47:57 +08:00
parent 708a3d5be8
commit 8789a76ecc
2 changed files with 166 additions and 113 deletions

207
README.md
View File

@@ -1,79 +1,130 @@
*** 项目要求 *** # 极简邀请函制作工具
一款以手机端为主、电脑端兼容的简易邀请函在线制作工具,无需安装、无需后端,打开浏览器即可使用。用户可上传背景图、添加文字、拖动位置,最终一键生成并保存完整邀请函图片。
核心功能
1. 设备适配
移动端优先设计,手机操作最流畅
电脑端完全兼容,可正常制作、编辑、保存
背景图自动适应屏幕大小,不变形、不拉伸
2. 预览区域规范
预览区域固定 3:5 比例
容器在页面居中显示,美观大方
背景图自适应铺满容器
3. 背景图上传
支持本地图片上传
支持 jpg /png/jpeg 格式
自动居左显示、自适应保留原有比例
4. 输入框功能
页面加载自动在右下角生成 2 个输入框,输入框为透明无边框设计
双击输入框 → 编辑文字
点击 / 长按输入框 → 可拖动位置
两个输入框完全独立,拖动互不影响
可随意拖拽到预览区域内任意位置
5. 操作交互优化
点击空白区域 → 自动取消输入框选中状态
未选中时无法编辑、无法拖动
避免误触、避免乱跑、避免重叠操作
6. 图片保存 / 导出(最重要)
电脑端:右键预览区域 → 自动合成并保存图片
手机端:长按预览区域 → 自动合成并保存图片
合成内容 = 背景图 + 所有输入框文字
输出一张完整的邀请函图片
***使用说明(最简流程)***
打开页面
上传背景图
双击输入框填写文字
单击输入框拖动到合适位置
电脑:右键保存图片
手机:长按保存图片
完成制作
## 要求记录 一款以手机端为主、电脑端兼容的简易邀请函在线制作工具。无需安装、无需后端,打开浏览器即可使用。用户可上传背景图、添加文字/图片/二维码、拖动调整位置,最终一键生成并保存完整邀请函图片。
1. 打开项目即显示 3:5 空白面板,背景图自动覆盖容器、超出部分自动裁剪,不拉伸、不变形
2. 要求两个独立输入框,可双击编辑、可拖动,文字背景透明无边框,放置在右下角 ---
3. 要求电脑端:右键预览区域可直接复制图片到剪贴板,移动端:长按预览区域可保存图片,操作简单,一键导出图片(复制 / 保存)
4. 预览的图片样式和保存的样式不一样,需要改成预览是什么样子,保存出来的图片内容就是什么样子 ## 项目结构
5. 要求鼠标右击或手机长按屏幕3:5预览区域时是保存图片的状态,点击其他区域不是保存状态
6. 要求图片和屏幕尺寸大小不一致时可在预览框中左右滑动图片调整位置,保持图片比例不变,点击空白处不选中任何输入框和图片状态,图片位置调整不影响文字编辑位置调整 ```
7. 文本字体颜色为黑色,字体为 "微软雅黑",字体大小为 16px,输入框放置在右下角 d:\projects\invitation-maker/
8. 打开项目时预览区域在屏幕居中显示,选择图片后图片在预览区居左显示,将底部请点击上方「添加图片」按钮上传背景图文字移到背景图预览白板提示中 ├── index.html # 主入口页面
9. 背景图选择后居左显示,图片原比例保存不变,可以左右滑动调节位置不能小于预览区宽度,预览区与图片合并到同一个图层中,不独立存在,以图片左右为边缘不溢出不拉伸,保持图片比例不变 ├── README.md # 项目说明文档
10. 输入框与背景图完全隔离:输入框的文字编辑、拖动、内容溢出,仅改变输入框自身的位置/大小,不带动背景图偏移,不撑大预览容器,背景图的滑动调整,不影响输入框的位置、编辑状态,操作互不干扰 ├── IMPROVEMENTS.md # 升级改进计划
11. 添加图片尺寸不一致时可以左右滑动图片调整位置,并且图片反转拖拽 ├── images/
12. 电脑端右键保存/复制/预览图片,手机端长按屏幕弹窗保存/复制/预览图片,可预览 │ └── default-bg.jpg # 默认背景图片
13. 顶部工具栏下方16px的位置,固定放置文本编辑器工具栏,加粗,斜体,字体颜色,字体,字号,可编辑文字样式 ├── css/
14. 字号逻辑调整,输入框字号只在自身原本字号上增减 │ ├── style.css # 主样式
15. 文本输入框添加辅助线,完善辅助线的吸附逻辑更加精准,辅助线颜色为红色 │ └── qr.css # 二维码功能样式
16. 顶部工具栏新增层叠图,可添加多张层叠图,单击选中状态可缩放删除自由移动位置,置于背景图之上,输入框与层叠图点击只能同时选中一个 └── js/
17. 层叠图添加辅助线 ├── script.js # 核心应用逻辑(~3970 行)
18. 可添加多个文本框,以及文本框删除功能 ├── qr.js # 二维码功能模块
19. 添加图片编辑器工具栏,图片编辑器与文本编辑器共用同一位置工具栏,选中对应元素切换对应编辑面板,点击空白处隐藏面板 └── inputs-config.js # 输入框初始化配置(独立配置文件)
20. 当选择文字的时候工具栏对应出现文字编辑器,当选择图片的时候工具栏对应出现图片编辑器,点击其他位置时隐藏工具栏,两个编辑栏的宽高保持和文本编辑器工具栏一致 ```
21. 调整图文工具栏布局,解决顶部遮挡问题,图片透明度滑块添加百分比
22. 给背景图添加编辑器,与层叠图编辑器文本编辑器通用一个工具栏,背景图工具栏功能为:背景图可以放大缩小,可以调节透明度,其余功能不受影响,点击层叠图只显示当前编辑器工具栏 ---
23. 校准画布编辑与图片导出文字垂直位置偏移问题,修改屏幕尺寸为960x1361像素
24. 添加新输入框,HTML文件里三个文本输入框改为随着添加背景图片后显示,三个输入框的位置分别为left:98px, top: 232.945px;left:98px, top: 275.555px;left:98px, top: 317.548px; ## 功能特性
25. 适配网站子目录部署,项目路由静态资源路径改为 /tool/invitation-maker/
26. 严格按照输入框功能不变,在这个基础上将三个输入框原本的默认文本,改为占位符,提示语为"请输入"的要求改 ### 核心功能
27. 缩小背景图/层叠图/文本按钮宽度,优化手机端保存图片直接存入相册功能
28. 设置默认背景图,页面打开自动加载,保留原有功能,点击背景图按钮可更换 | 功能 | 说明 |
29. 将编辑器工具栏颜色,字体,字号间距缩小一些,美观大方 |------|------|
30. 缩小编辑器工具栏间距,适应小屏幕 | **背景图** | 上传 jpg/png 图片,自动适应预览区域,保持原比例,可左右滑动调整位置,支持缩放和透明度调节 |
31. 适配微信浏览器优化导出图片质量,优化画布尺寸计算逻辑以保持宽高比 | **默认背景** | 页面打开自动加载默认背景图,点击「背景图」按钮可更换 |
32. 限制边界不强制吸附,优化移动端编辑交互逻辑,解决ios设备上键盘无法弹出的问题,优化统一桌面和移动端触摸编辑和键盘触发逻辑 | **文字编辑** | 3 个预设输入框(企业、嘉宾、编号),支持添加更多文本,双击编辑文字,单击拖拽位置 |
33. 导出高清背景图文本的缩放计算逻辑,优化编辑栏固定与交互,改进字体加载和文本渲染逻辑,支持在选中元素上应用样式 | **文本样式** | 加粗、斜体、字体选择、字号选择、颜色选择,通过编辑器工具栏操作 |
34. 调整输入框位置,优化字体设置与选择器匹配逻辑,添加输入框字体加粗样式 | **层叠图** | 可添加多张层叠图片,置于背景图之上,支持拖动、缩放、旋转(含 0°/90°/-90° 吸附)、透明度、水平镜像、删除 |
35. 添加层叠图片旋转功能,为旋转滑动条添加0度,90度,-90度吸附功能,数值单独占位 | **二维码** | 弹窗输入内容生成二维码,支持拖动、缩放、透明度调节,可添加到画布任意位置 |
36. 层叠图镜像功能,优化编辑器工具栏按钮样式间距,调整背景图缩放和透明度顺序 | **图层管理** | 侧边栏图层面板,显示所有元素层级,支持顺序调整(上移/下移/置顶/置底)、可见性切换、删除 |
37. 项目添加撤销重做功能,检查修复撤销重做后背景图和文本位置显示异常问题,撤销重做按钮只需要点击一次就能执行操作 | **撤销/重做** | 支持操作历史撤销(Ctrl+Z)与重做(Ctrl+Y) |
38. 优化撤销功能,优化顶部编辑栏按钮占位,优化撤销/重做按钮与文本按钮和保存/预览按钮的间距,文本按钮 ↔ [4px] ↔ 分隔线 ↔ 撤销按钮 ↔ [3px] ↔ 重做按钮 ↔ 分隔线 ↔ [4px] ↔ 保存按钮 | **辅助线** | 拖拽元素时红色吸附对齐辅助线 |
39. 优化顶部工具栏样式,调整撤销重做按钮分隔栏边距,重构层叠图渲染到最终保存图片上的逻辑 | **网格线** | 可选显示 10px 网格辅助线 |
| **元素锁定** | 锁定后不可拖动/编辑(锁定状态黄色虚线) |
| **位置信息** | 选中或拖拽输入框时,上方显示实时 left/top 坐标 |
### 编辑器工具栏
编辑器工具栏固定于顶部工具栏下方,根据选中元素自动切换:
| 选中元素 | 显示的工具 |
|---------|-----------|
| 文字输入框 | 加粗、斜体、字体、字号、颜色、删除、锁定 |
| 层叠图 | 透明度、旋转、水平镜像、缩放、删除、锁定 |
| 背景图 | 缩放、透明度 |
| 二维码 | 透明度、缩放 |
### 图片保存/导出
- **电脑端**:右键预览区域 → 合成并保存/复制/预览图片
- **手机端**:长按预览区域或点击「保存/预览」按钮 → 弹窗选择保存/预览
- **输出格式**:高清 PNG,至少 2x 分辨率(最小 1080x1920),所见即所得
- **微信兼容**:针对微信内置浏览器优化画布尺寸限制和渲染逻辑
### 输入框配置
三个预设输入框的初始化位置和样式通过独立配置文件 `js/inputs-config.js` 管理,可直接修改无需改动主逻辑:
```js
// input1 - 企业(left: 100px, top: 218.4px)
// input2 - 嘉宾(left: 100px, top: 261.4px)
// input3 - 编号(left: 100px, top: 308.1px)
```
---
## 使用说明
### 最简流程
1. 打开页面(自动加载默认背景图)
2. 上传背景图(可选,点击「背景图」按钮更换)
3. 双击输入框填写文字
4. 单击输入框拖动到合适位置
5. 电脑:右键预览区域保存/复制图片
6. 手机:长按预览区域或点击「保存/预览」保存图片
### 进阶操作
- **添加层叠图**:点击「层叠图」按钮,选中后可拖动/缩放/旋转/镜像
- **添加文本框**:点击「文本」按钮,可任意添加更多文字元素
- **添加二维码**:点击左下角「二维码」按钮,输入内容生成
- **图层管理**:点击「图层」按钮打开右侧面板,可调整顺序和可见性
- **撤销/重做**:使用顶部工具栏的撤销/重做按钮或快捷键(Ctrl+Z/Ctrl+Y)
---
## 预览区域
- 固定 3:5 宽高比(960x1361)
- 容器最大宽度 360px,页面居中显示
- 背景图自适应铺满容器,超出部分自动裁剪
---
## 技术栈
- **原生 HTML/CSS/JS**,零外部依赖
- 使用 `canvas` 进行高清无损合成导出
- 图片缓存系统(最大缓存 20 张)
- 内存监控与自动清理
- 防抖/节流优化
- **二维码**:基于 qrcodejs 库生成
- 所有资源均可在子目录部署(如 `/tool/invitation-maker/`)
---
## 开发计划
详见 `IMPROVEMENTS.md`,主要方向:
- 代码模块化重构
- 模板系统
- 更多元素类型(形状、贴纸等)
- 高级文本功能
- 图片滤镜与裁剪
- 自动保存与草稿
- 暗黑模式
---
*最后更新: 2026-05-15*

View File

@@ -1931,7 +1931,9 @@
// 更新字号选择器 // 更新字号选择器
const fontSizeValueEl = fontSizePicker.querySelector('.roller-value'); const fontSizeValueEl = fontSizePicker.querySelector('.roller-value');
if (fontSizeValueEl) { if (fontSizeValueEl) {
const displaySize = isNaN(fontSize) ? 16 : fontSize; // 优先读取 inline style(用户调整过的字号),再回退到 computed style
const inlineSize = parseInt(element.style.fontSize);
const displaySize = inlineSize || (isNaN(fontSize) ? 16 : fontSize);
fontSizeValueEl.textContent = displaySize.toString(); fontSizeValueEl.textContent = displaySize.toString();
fontSizeValue = displaySize; fontSizeValue = displaySize;
} }
@@ -2451,47 +2453,47 @@
const fontSizeUpBtn = fontSizeRoller.querySelector('.roller-up'); const fontSizeUpBtn = fontSizeRoller.querySelector('.roller-up');
const fontSizeDownBtn = fontSizeRoller.querySelector('.roller-down'); const fontSizeDownBtn = fontSizeRoller.querySelector('.roller-down');
function updateFontSize(newSize, forceUpdate = false) { // 获取当前选中输入框的实际字号
function getCurrentFontSize() {
var target = editingInput || selectedInput;
if (!target) return fontSizeValue;
var size = parseInt(window.getComputedStyle(target).fontSize);
return isNaN(size) ? 16 : size;
}
function updateFontSize(newSize, forceUpdate) {
newSize = Math.max(10, Math.min(42, newSize)); newSize = Math.max(10, Math.min(42, newSize));
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (!target) return;
if (forceUpdate || targetInput) { target.style.fontSize = newSize + 'px';
targetInput.style.fontSize = newSize + 'px';
fontSizeValue = newSize;
fontSizeValueEl.textContent = newSize;
} else {
fontSizeValue = newSize; fontSizeValue = newSize;
fontSizeValueEl.textContent = newSize; fontSizeValueEl.textContent = newSize;
} }
}
fontSizeUpBtn.addEventListener('click', () => { fontSizeUpBtn.addEventListener('click', function() {
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (targetInput) { if (!target) return;
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; var currentSize = parseInt(target.style.fontSize) || parseInt(window.getComputedStyle(target).fontSize) || fontSizeValue;
updateFontSize(currentSize + 1, true); updateFontSize(currentSize + 1);
saveState(); saveState();
}
}); });
fontSizeDownBtn.addEventListener('click', () => { fontSizeDownBtn.addEventListener('click', function() {
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (targetInput) { if (!target) return;
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; var currentSize = parseInt(target.style.fontSize) || parseInt(window.getComputedStyle(target).fontSize) || fontSizeValue;
updateFontSize(currentSize - 1, true); updateFontSize(currentSize - 1);
saveState(); saveState();
}
}); });
fontSizeRoller.addEventListener('wheel', (e) => { fontSizeRoller.addEventListener('wheel', function(e) {
e.preventDefault(); e.preventDefault();
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (targetInput) { if (!target) return;
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; var currentSize = parseInt(target.style.fontSize) || parseInt(window.getComputedStyle(target).fontSize) || fontSizeValue;
const delta = e.deltaY > 0 ? -1 : 1; var delta = e.deltaY > 0 ? -1 : 1;
updateFontSize(currentSize + delta, true); updateFontSize(currentSize + delta);
saveState(); saveState();
}
}, { passive: false }); }, { passive: false });
} }