Compare commits

..

41 Commits

Author SHA1 Message Date
55dfeed2c2 feat: 多模板支持,URL 查询参数 ?t= 切换不同邀请函配置 2026-05-16 09:43:44 +08:00
9e0a7c083d feat: config.js 支持 isLocked 配置,锁定仅限制位置不影响编辑 2026-05-15 15:45:45 +08:00
396559b2ec fix: deselectAll 补上 selectedOverlay 清理 2026-05-15 15:27:55 +08:00
7c6128dd6d fix: touchstart 时立即取消输入框选中,不依赖 click 事件链 2026-05-15 15:26:59 +08:00
caa107ff64 chore: 添加静态资源缓存版本号 v20260515 2026-05-15 15:21:20 +08:00
30886888ba fix: 补上 bgLayer.click 的输入框取消选中逻辑 2026-05-15 15:18:27 +08:00
be00d6e3a9 fix: 点击背景图时取消输入框选中状态 2026-05-15 15:16:28 +08:00
f090f18a9d refactor: inputs-config.js 合并为 config.js,新增默认背景图配置及使用说明 2026-05-15 15:14:11 +08:00
315f568138 feat: 粗细滑块、移动端两行工具栏、二维码底部定位、字号修复及README更新 2026-05-15 15:06:02 +08:00
8789a76ecc fix(editor): 修复字号读取及工具栏逻辑
重构编辑器工具栏字号调整逻辑,优先读取内联样式字号,
修复字号显示不同步问题,并更新 README 文档
2026-05-15 14:47:57 +08:00
708a3d5be8 style(ui): 调整位置信息提示框样式 2026-05-15 14:40:11 +08:00
c6f7787c35 feat: 抽离输入框配置并添加拖拽位置提示 2026-05-15 14:35:51 +08:00
Zhukj
6a0294f0c0 feat(图层管理): 为二维码添加图层支持并实现相关功能
- 在状态管理中新增 qrCounter 用于生成唯一图层 ID
- 为二维码元素添加 dataset.layerId 属性
- 实现二维码图层更新事件机制
- 在图层列表中显示二维码元素并支持选中操作
- 完善二维码的绘制逻辑,支持透明度设置
2026-04-27 08:46:49 +08:00
Zhukj
4ee95df792 feat(二维码): 添加删除按钮并重构二维码元素结构
- 为二维码元素添加删除按钮,仅在选中时显示
- 重构二维码元素结构,将图片包裹在容器div中
- 调整透明度设置逻辑以匹配新结构
- 防止删除按钮触发拖拽事件
2026-04-25 17:53:22 +08:00
Zhukj
c7963cec6e feat(qr): 添加二维码工具栏并优化交互体验
添加二维码编辑工具栏,支持透明度和缩放调整
优化选中状态和拖拽交互,增强用户体验
重构代码结构,提高可维护性
2026-04-25 16:51:27 +08:00
Zhukj
37aaec1e42 fix: 阻止输入框编辑时的事件冒泡
避免在编辑输入框时触发容器的点击事件
2026-04-25 15:43:02 +08:00
Zhukj
02f3adb936 feat(邀请函): 添加二维码生成功能
- 新增二维码按钮及弹窗界面
- 实现二维码生成、预览和添加到画布功能
- 支持二维码在画布上的拖拽操作
- 添加相关样式和事件处理
2026-04-25 15:13:15 +08:00
Zhukj
81f63781a9 refactor(图层管理): 重构图层列表更新逻辑,按指定顺序显示图层 2026-04-25 10:05:38 +08:00
Zhukj
79203d418b style(css): 调整工具栏和按钮的尺寸及间距
统一缩小工具栏和按钮的尺寸,优化整体视觉间距
2026-04-25 09:46:49 +08:00
Zhukj
b380e65161 feat(图层面板): 添加点击空白区域关闭面板功能
添加点击面板外部区域关闭图层面板的功能,同时支持桌面端和移动端。桌面端使用 click 事件,移动端使用 touchend 事件,并确保点击面板内部或图层按钮时不触发关闭。
2026-04-25 09:35:32 +08:00
Zhukj
a7d0ad8e51 feat(图层管理): 添加图层管理面板及功能
- 新增图层面板UI组件,支持显示/隐藏图层
- 实现图层列表动态更新和选择功能
- 为所有图层元素添加唯一标识和隐藏状态控制
- 移除原有删除按钮,改为通过图层管理
- 在保存/恢复状态时处理图层可见性
- 优化预览生成逻辑,跳过隐藏图层
2026-04-25 09:31:21 +08:00
Zhukj
ef6010a8b2 perf: 优化图片渲染性能和内存管理
重构图片渲染逻辑,引入图片缓存系统和状态压缩机制
添加批量处理层叠图功能,每批5个图片处理
实现内存监控和自动清理机制
增加全局Loading管理器显示操作进度
优化撤销重做功能的状态比较和保存逻辑
2026-04-23 16:08:31 +08:00
Zhukj
b9fadbd31e docs: 更新 README 添加未来优化计划列表 2026-04-23 15:12:00 +08:00
Zhukj
a732428a70 docs: 更新完善邀请函编辑器迭代需求文档 2026-04-22 18:29:02 +08:00
Zhukj
d25ee16d3b style(css): 调整工具栏分隔线边距从6px改为4px 2026-04-22 15:05:41 +08:00
Zhukj
c28f37e81e style(css): 调整工具栏分隔线和按钮组的边距
- 移除左右分隔线的1px边距,使间距更一致
- 为撤销重做按钮组添加flex布局和间隙
2026-04-22 14:59:32 +08:00
Zhukj
f64e83e8cd style(工具栏): 调整撤销重做按钮组样式并添加分隔线
- 将撤销重做按钮放入独立容器并设置间距
- 添加左右分隔线样式及不同边距
- 调整保存按钮的尺寸和间距
2026-04-22 14:43:18 +08:00
Zhukj
4c898306e0 refactor(UI): 优化工具栏样式并重构层叠图恢复逻辑
调整工具栏按钮和间距样式以提升视觉一致性
将层叠图恢复逻辑重构为独立函数以提高代码复用性
2026-04-22 14:20:01 +08:00
Zhukj
1983c15fb9 feat(editor): 添加撤销重做功能
实现完整的撤销重做功能,包括:
1. 添加工具栏撤销/重做按钮及快捷键支持
2. 实现状态保存与恢复机制
3. 添加状态变化检测与历史记录管理
4. 更新相关UI交互以触发状态保存
2026-04-22 14:08:42 +08:00
Zhukj
e46805deca refactor: 移除长按保存图片功能及相关代码
移除不再需要的长按保存图片功能,包括HTML中的提示元素、CSS样式以及JS中的相关逻辑
将保存图片提示改为点击保存,简化用户操作流程
2026-04-22 11:17:31 +08:00
Zhukj
848277849a feat(画布): 添加图片旋转和镜像变换支持
新增 drawImageWithTransform 函数处理图片的旋转和镜像变换,修改图片绘制逻辑以支持这些效果
2026-04-22 10:57:29 +08:00
Zhukj
9d9c4e4186 refactor(界面): 调整背景图工具中缩放和透明度的顺序
将背景图编辑工具中的缩放控件和透明度控件位置互换,使界面布局更合理
2026-04-22 09:54:40 +08:00
Zhukj
8734a48b76 style(css): 调整镜像按钮的尺寸和字体大小
将镜像按钮的宽度和高度设置为30px,并将字体大小从18px调整为16px,以保持视觉一致性
2026-04-22 09:12:54 +08:00
Zhukj
13a282d93a feat(工具栏): 添加水平镜像功能并调整工具栏样式
- 在工具栏添加水平镜像按钮
- 调整工具栏间距和分割线样式
- 实现图片水平镜像功能及状态同步
2026-04-21 18:28:43 +08:00
Zhukj
0665987a03 feat(旋转控制): 为旋转滑块添加吸附功能并固定数值显示宽度
在旋转滑块附近0°、-90°、90°时添加5度的吸附阈值,使操作更精准
为旋转度数和百分比数值显示元素设置固定宽度,保持布局稳定
2026-04-21 16:30:51 +08:00
Zhukj
da2f3cfbff feat: 添加图片旋转功能
- 在工具栏中添加旋转滑块控件
- 实现图片旋转功能及角度显示
- 支持读取和更新当前选中图片的旋转角度
2026-04-21 16:17:14 +08:00
Zhukj
89b7f0162f Merge branch 'master' of https://git.aigc-quickapp.com/WeWork/invitation-maker 2026-04-21 14:28:44 +08:00
Zhukj
cd991c33d3 style(css): 调整工具栏组件的宽度和对齐方式
- 为工具栏组添加100%宽度和居中对齐
- 统一工具栏按钮容器的宽度为100%
2026-04-21 14:27:21 +08:00
Zhukj
ce356586a3 style(css): 调整工具栏标签字体大小及显示规则
将工具栏标签字体大小从11px调整为12px,并修改第一个子元素的显示规则为inline以提升可读性
2026-04-21 14:23:47 +08:00
Zhukj
1341152002 docs: 更新补充README迭代需求说明文档 2026-04-21 11:45:02 +08:00
4f62f61fce style(ui): 添加输入框字体加粗样式 2026-04-21 10:59:16 +08:00
10 changed files with 3316 additions and 318 deletions

295
IMPROVEMENTS.md Normal file
View File

@@ -0,0 +1,295 @@
# 极简邀请函制作工具 - 升级改进计划
## 一、架构与工程化改进
### 1. 模块化重构
- **现状**: 所有代码集中在 3000+ 行的 `script.js` 文件中
- **改进方案**: 拆分为多个功能模块
- `state.js` - 状态管理(撤销/重做栈、当前选中元素等)
- `canvas.js` - 画布渲染与导出逻辑
- `textEditor.js` - 文本编辑功能
- `imageEditor.js` - 图片编辑功能
- `ui.js` - UI 交互与事件绑定
- `utils.js` - 工具函数
### 2. 引入构建工具
- **现状**: 原生 HTML/CSS/JS,无构建流程
- **改进方案**: 引入 Vite 或 Webpack
- 支持 ES6 Modules
- 代码压缩与优化
- 热更新开发体验
- 生产环境自动构建
### 3. TypeScript 迁移
- **改进方案**: 将 JavaScript 迁移至 TypeScript
- 添加类型定义,减少运行时错误
- 增强 IDE 智能提示
- 提升代码健壮性和可维护性
---
## 二、功能增强
### 4. 图层管理系统
- **现状**: 仅支持背景图、层叠图、文字三层简单叠加
- **改进方案**:
- 图层列表面板(显示所有元素层级)
- 图层顺序调整(上移/下移/置顶/置底)
- 图层命名与锁定功能
- 图层可见性切换(显示/隐藏)
- 图层透明度独立调节
### 5. 更多元素类型
- **新增元素**:
- **形状元素**: 矩形、圆形、三角形、线条、箭头
- **二维码/条形码**: 支持输入内容生成二维码
- **图标库**: 集成 FontAwesome 或自定义图标库
- **贴纸/装饰**: 预设装饰元素库(花边、边框、图案)
- **日期时间**: 自动插入当前日期,支持多种格式
### 6. 高级文本功能
- **现状**: 仅支持基础字体、颜色、大小、加粗、斜体
- **改进方案**:
- 文本对齐方式(左对齐/居中/右对齐/两端对齐)
- 行高调整
- 字间距调整
- 文本阴影/描边效果
- 富文本编辑器(支持部分文字独立设置样式)
- 文字竖排支持
- 文字沿路径排列
### 7. 图片编辑增强
- **现状**: 仅支持透明度、旋转、水平镜像
- **改进方案**:
- 图片滤镜(亮度、对比度、饱和度、色相、模糊、灰度)
- 图片裁剪功能(自由裁剪、固定比例裁剪)
- 垂直镜像
- 圆角裁剪
- 旋转角度精确输入(支持小数角度)
- 图片混合模式(正片叠底、滤色、叠加等)
### 8. 模板系统
- **改进方案**:
- 预设邀请函模板库(婚礼、生日、会议、活动、节日等)
- 模板分类与搜索
- 用户自定义模板保存
- 模板分享功能
- 热门模板推荐
---
## 三、性能优化 ✅ 已完成
### 9. Canvas 渲染优化 ✅
- **现状**: `generateCanvas` 函数逻辑复杂,渲染耗时
- **改进方案**:
- ✅ 使用 `OffscreenCanvas` 进行后台渲染
- ✅ 图片预加载与缓存机制(新增 `imageCache` Map)
- ✅ 大图片分片渲染(批量处理,每批5个)
- ✅ 渲染进度提示(Loading 管理器实时更新进度)
- **实现细节**:
- 新增 `getCachedImage()` 函数缓存已加载图片
- `drawOverlayImagesHD()` 改为异步批量处理
- 每批处理后让出主线程,避免阻塞 UI
### 10. 内存管理优化 ✅
- **现状**: 历史状态使用深拷贝,可能占用大量内存
- **改进方案**:
- ✅ 状态压缩(`compressState()` 限制文本长度)
- ✅ 根据历史记录数量动态清理(超过80%时自动清理到60%)
- ✅ 图片缓存大小限制(最大20张)
- ✅ 提供清理缓存功能(`clearImageCache()`)
- **实现细节**:
- 新增 `memoryMonitor` 内存监控器,每30秒检查一次
- 状态对比优化(先比较时间戳和关键属性)
- 状态添加时间戳便于管理
### 11. 防抖/节流优化 ✅
- **现状**: 拖拽过程中频繁触发状态保存
- **改进方案**:
- ✅ 拖拽结束只保存一次(使用 `immediate` 模式)
- ✅ 中间过程不保存状态
- ✅ 大操作使用 loading 提示(`loadingManager`)
- ✅ 性能计时(`performance.now()` 记录耗时)
- **实现细节**:
- 新增 `throttle()` 和 `debounce()` 工具函数
- 新增 `loadingManager` 全局 Loading 管理器
- 图片保存过程显示详细进度(初始化画布→绘制背景→绘制层叠图→绘制文字)
- 控制台输出详细性能日志
---
## 四、用户体验改进
### 12. 移动端优化
- **现状**: 基础移动端适配已具备
- **改进方案**:
- 触摸手势支持(双指缩放、旋转)
- 底部操作栏适配 iPhone 安全区
- 横屏模式支持
- 触摸反馈优化
- 虚拟键盘适配优化
### 13. 键盘快捷键扩展
- **现状**: 仅支持撤销/重做和方向键微调
- **改进方案**:
- `Delete` / `Backspace` - 删除选中元素
- `Ctrl+C` / `Ctrl+V` - 复制粘贴元素
- `Ctrl+D` - 复制元素
- `Ctrl+G` - 组合/解组元素
- `Esc` - 取消选中/退出编辑
- `Ctrl+S` - 保存草稿
- `Ctrl+A` - 全选元素
- `↑↓←→` + `Shift` - 快速移动(10px)
- `↑↓←→` + `Alt` - 微调移动(0.5px)
### 14. 对齐与分布工具
- **改进方案**:
- 多选元素对齐(左对齐、水平居中、右对齐、顶部对齐、垂直居中、底部对齐)
- 元素等间距分布(水平分布、垂直分布)
- 相对于画布居中
- 智能对齐线增强(显示距离数值)
- 网格吸附(可选开启)
### 15. 预览与导出增强
- **现状**: 仅支持 PNG 格式导出
- **改进方案**:
- 多种导出格式(PNG、JPG、WebP、PDF)
- 导出质量选择(低/中/高/无损)
- 导出尺寸预设(微信头像、朋友圈、打印尺寸、A4等)
- 实时预览模式
- 导出前预览确认
- 批量导出功能
### 16. 自动保存与草稿
- **改进方案**:
- LocalStorage 自动保存(可设置间隔)
- 草稿恢复提示(意外关闭后)
- 多草稿管理(命名、删除、切换)
- 手动保存快照
---
## 五、代码质量改进
### 17. 错误处理
- **现状**: 异步操作缺少完善的错误处理
- **改进方案**:
- 添加全局错误边界
- 图片加载失败优雅降级
- 网络异常处理
- 用户友好的错误提示
### 18. 事件委托优化
- **现状**: 大量事件监听器直接绑定在元素上
- **改进方案**:
- 使用事件委托减少内存占用
- 统一事件管理
- 避免内存泄漏
### 19. CSS 优化
- **现状**: 使用传统 CSS 文件
- **改进方案**:
- 使用 CSS 变量统一管理颜色、间距、阴影等
- 考虑使用 CSS-in-JS 或 Tailwind CSS
- 添加暗黑模式支持
- 响应式断点优化
### 20. 无障碍支持 (a11y)
- **改进方案**:
- 添加 ARIA 标签
- 键盘导航支持
- 屏幕阅读器适配
- 高对比度模式
- 字体大小调节支持
---
## 六、可扩展性
### 21. 插件系统
- **改进方案**:
- 设计插件接口规范
- 允许第三方开发插件
- 插件市场/管理器
- 预设组件库扩展
### 22. 数据持久化
- **改进方案**:
- 后端服务集成
- 用户账号系统
- 云端保存作品
- 作品分享链接
- 团队协作功能
### 23. 国际化 (i18n)
- **改进方案**:
- 多语言支持(中文、英文、日文等)
- RTL 布局适配
- 时区适配
- 本地化日期格式
---
## 优先级规划
### 🔴 高优先级(核心改进)
| 序号 | 改进项 | 预期收益 |
|------|--------|----------|
| 1 | 模块化重构 | 提升代码可维护性,便于团队协作 |
| 2 | TypeScript 迁移 | 减少 Bug,提升开发效率 |
| 3 | 图层管理系统 | 核心功能增强,用户体验大幅提升 |
| 4 | 自动保存与草稿 | 防止数据丢失,提升用户体验 |
| 5 | 错误处理完善 | 提升应用稳定性 |
### 🟡 中优先级(功能扩展)
| 序号 | 改进项 | 预期收益 |
|------|--------|----------|
| 6 | 模板系统 | 降低用户使用门槛,提升效率 |
| 7 | 更多元素类型 | 丰富设计能力 |
| 8 | 高级文本功能 | 满足专业排版需求 |
| 9 | 图片编辑增强 | 减少对外部工具的依赖 |
| 10 | 键盘快捷键扩展 | 提升专业用户效率 |
| 11 | 对齐与分布工具 | 提升排版效率 |
### 🟢 低优先级(长期规划)
| 序号 | 改进项 | 预期收益 |
|------|--------|----------|
| 12 | 插件系统 | 构建生态,无限扩展可能 |
| 13 | 后端集成 | 实现数据云同步和社交功能 |
| 14 | 国际化 | 拓展海外市场 |
| 15 | 暗黑模式 | 提升视觉体验 |
| 16 | 无障碍支持 | 社会责任,覆盖更多用户 |
---
## 实施建议
### 阶段一:基础重构(1-2周)
1. 引入 Vite 构建工具
2. 代码模块化拆分
3. TypeScript 迁移
4. 完善错误处理
### 阶段二:核心功能(2-3周)
1. 图层管理系统
2. 自动保存与草稿
3. 对齐与分布工具
4. 键盘快捷键扩展
### 阶段三:功能扩展(3-4周)
1. 模板系统
2. 更多元素类型
3. 高级文本功能
4. 图片编辑增强
### 阶段四:优化与扩展(持续)
1. 性能优化
2. 移动端优化
3. 插件系统
4. 后端集成
---
*文档生成时间: 2026-04-23*
*基于项目版本分析*

190
README.md
View File

@@ -1,80 +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,输入框放置在右下角
8. 打开项目时预览区域在屏幕居中显示,选择图片后图片在预览区居左显示,将底部请点击上方「添加图片」按钮上传背景图文字移到背景图预览白板提示中
9. 背景图选择后居左显示,图片原比例保存不变,可以左右滑动调节位置不能小于预览区宽度,预览区与图片合并到同一个图层中,不独立存在,以图片左右为边缘不溢出不拉伸,保持图片比例不变
10. 输入框与背景图完全隔离:输入框的文字编辑、拖动、内容溢出,仅改变输入框自身的位置/大小,不带动背景图偏移,不撑大预览容器,背景图的滑动调整,不影响输入框的位置、编辑状态,操作互不干扰
11. 添加图片尺寸不一致时可以左右滑动图片调整位置,并且图片反转拖拽
12. 电脑端右键保存/复制/预览图片,手机端长按屏幕弹窗保存/复制/预览图片,可预览
13. 顶部工具栏下方16px的位置,固定放置文本编辑器工具栏,加粗,斜体,字体颜色,字体,字号,可编辑文字样式
14. 字号逻辑调整,输入框字号只在自身原本字号上增减
15. 文本输入框添加辅助线,完善辅助线的吸附逻辑更加精准,辅助线颜色为红色
16. 顶部工具栏新增层叠图,可添加多张层叠图,单击选中状态可缩放删除自由移动位置,置于背景图之上,输入框与层叠图点击只能同时选中一个
17. 层叠图添加辅助线
18. 可添加多个文本框,以及文本框删除功能
19. 添加图片编辑器工具栏,图片编辑器与文本编辑器共用同一位置工具栏,选中对应元素切换对应编辑面板,点击空白处隐藏面板
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. 将编辑器工具栏颜色,字体,字号间距缩小一些,美观大方
一款以手机端为主、电脑端兼容的简易邀请函在线制作工具。无需安装、无需后端,打开浏览器即可使用。用户可上传背景图、添加文字/图片/二维码、拖动调整位置,最终一键生成并保存完整邀请函图片。
---
## 项目结构
```
d:\projects\invitation-maker/
├── index.html # 主入口页面
├── README.md # 项目说明文档
├── IMPROVEMENTS.md # 升级改进计划
├── images/
│ └── default-bg.jpg # 默认背景图片
├── css/
│ ├── style.css # 主样式
│ └── qr.css # 二维码功能样式
└── js/
├── script.js # 核心应用逻辑(~3970 行)
├── qr.js # 二维码功能模块
└── inputs-config.js # 输入框初始化配置(独立配置文件)
```
---
## 功能特性
## 需要注意的点
### 核心功能
1. 图片要必须能左右滑动调整位置,保持图片比例不变,图片任何内容都可以被拖动到预览区域内任意位置
| 功能 | 说明 |
|------|------|
| **背景图** | 上传 jpg/png 图片,自动适应预览区域,保持原比例,可左右滑动调整位置,支持缩放和透明度调节 |
| **默认背景** | 页面打开自动加载默认背景图,点击「背景图」按钮可更换 |
| **文字编辑** | 3 个预设输入框(企业、嘉宾、编号),支持添加更多文本,双击编辑文字,单击拖拽位置 |
| **文本样式** | 加粗、斜体、字体选择、字号选择、颜色选择,通过编辑器工具栏操作 |
| **层叠图** | 可添加多张层叠图片,置于背景图之上,支持拖动、缩放、旋转(含 0°/90°/-90° 吸附)、透明度、水平镜像、删除 |
| **二维码** | 弹窗输入内容生成二维码,支持拖动、缩放、透明度调节,可添加到画布任意位置 |
| **图层管理** | 侧边栏图层面板,显示所有元素层级,支持顺序调整(上移/下移/置顶/置底)、可见性切换、删除 |
| **撤销/重做** | 支持操作历史撤销(Ctrl+Z)与重做(Ctrl+Y) |
| **辅助线** | 拖拽元素时红色吸附对齐辅助线 |
| **网格线** | 可选显示 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*

189
css/qr.css Normal file
View File

@@ -0,0 +1,189 @@
/* 二维码功能样式 */
/* ========== 悬浮按钮 ========== */
#qr-btn {
position: fixed;
bottom: 20px;
left: 20px;
width: 44px;
height: 44px;
padding: 0;
border-radius: 50%;
background: #4a90e2;
color: #fff;
border: none;
font-size: 12px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
display: none;
white-space: nowrap;
}
#qr-btn.show {
display: block;
}
#qr-btn:hover {
background: #3a7bc8;
}
/* ========== 弹窗 ========== */
#qr-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
justify-content: center;
align-items: center;
}
#qr-modal.active {
display: flex;
}
.qr-box {
background: #fff;
border-radius: 8px;
width: 320px;
max-width: 90%;
}
.qr-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.qr-header h3 {
margin: 0;
font-size: 16px;
}
.qr-close {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
}
.qr-body {
padding: 20px;
}
#qr-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
#qr-preview {
margin-top: 15px;
min-height: 128px;
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
border-radius: 4px;
}
#qr-preview canvas {
display: block;
}
.qr-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid #eee;
}
.qr-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.qr-cancel {
background: #f0f0f0;
color: #333;
}
.qr-ok {
background: #4a90e2;
color: #fff;
}
/* ========== 画布上的二维码 ========== */
.qr-item {
position: absolute;
cursor: move;
user-select: none;
touch-action: none;
}
/* 选中状态 */
.qr-item.qr-selected {
outline: 2px solid #4a90e2;
outline-offset: 2px;
}
/* 删除按钮 */
.qr-delete-btn {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
background: #ff4444;
color: #fff;
border: none;
border-radius: 50%;
font-size: 16px;
line-height: 20px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
z-index: 100;
padding: 0;
margin: 0;
}
.qr-item.qr-selected .qr-delete-btn {
display: flex;
}
.qr-delete-btn:hover {
background: #cc0000;
}
/* ========== 二维码工具栏 - 与背景图工具栏保持一致 ========== */
.qr-tools {
display: none;
}
.qr-tools.active {
display: flex;
}
/* 数值显示固定宽度,单独占位 */
#qrOpacityValue, #qrScaleValue {
display: inline-block;
min-width: 40px;
text-align: center;
}

View File

@@ -23,18 +23,19 @@ html, body {
.toolbar {
display: flex;
justify-content: center;
gap: 8px;
padding: 12px 16px;
align-items: center;
gap: 4px;
padding: 10px 12px;
background: #fff;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.btn {
min-width: 72px;
height: 40px;
padding: 0 12px;
font-size: 15px;
min-width: 56px;
height: 32px;
padding: 0 8px;
font-size: 13px;
border: 1px solid #ddd;
border-radius: 6px;
background: #fff;
@@ -52,7 +53,7 @@ html, body {
}
.btn-primary {
min-width: 100px;
min-width: 80px;
background: #4a90e2;
color: #fff;
border-color: #4a90e2;
@@ -62,6 +63,80 @@ html, body {
background: #3a7bc8;
}
/* 工具栏垂直分隔线 */
.toolbar-divider-v {
width: 1px;
height: 18px;
background: #e0e0e0;
margin: 0 1px;
}
/* 工具栏图标按钮 */
.toolbar-btn-icon {
width: 24px;
height: 24px;
font-size: 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
color: #555;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.toolbar-btn-icon:hover {
background: #f5f5f5;
border-color: #ccc;
}
.toolbar-btn-icon:active {
background: #e8e8e8;
}
.toolbar-btn-icon:disabled {
opacity: 0.3;
cursor: not-allowed;
background: #f5f5f5;
}
/* 撤销重做按钮组 */
.toolbar-action-group {
display: flex;
gap: 2px;
margin: 0 1px;
}
/* 左侧分隔线(文本按钮后) */
.toolbar-divider-v.divider-left {
margin-left: 2px;
margin-right: 0;
}
/* 右侧分隔线(保存按钮前) */
.toolbar-divider-v.divider-right {
margin-left: 0;
margin-right: 4px;
}
/* 撤销重做按钮组 */
.toolbar-action-group {
display: flex;
gap: 3px;
margin: 0;
}
/* 保存/预览按钮 */
.btn-primary {
min-width: 80px;
height: 36px;
padding: 0 14px;
font-size: 14px;
}
/* 文本输入框删除按钮 */
.text-input .text-delete-btn {
position: absolute;
@@ -88,7 +163,7 @@ html, body {
/* 编辑器工具栏(文本/图片共用) */
.editor-toolbar {
position: absolute;
top: 65px;
top: 52px;
left: 0;
right: 0;
display: flex;
@@ -113,37 +188,144 @@ html, body {
z-index: 10000;
}
/* 移动端:允许横向滚动 */
/* 移动端:两行整洁布局 @ 375px 友好 */
@media (max-width: 768px) {
.editor-toolbar {
justify-content: flex-start;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding: 8px;
}
.editor-toolbar::-webkit-scrollbar {
display: none;
justify-content: center;
overflow: visible;
padding: 6px 10px;
height: auto;
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
}
.toolbar-group {
gap: 2px;
flex-shrink: 0;
gap: 6px;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 100%;
padding: 4px 0;
}
/* 分隔线在折叠布局中隐藏 */
.toolbar-divider {
display: none;
}
.toolbar-btn {
width: 32px;
height: 32px;
width: 30px;
height: 30px;
font-size: 14px;
min-width: 0;
border-radius: 6px;
background: #f5f5f5;
border-color: #e0e0e0;
}
.toolbar-btn:active {
background: #e0e0e0;
transform: scale(0.95);
}
.toolbar-btn.active {
background: #4a90e2;
color: #fff;
border-color: #4a90e2;
}
.toolbar-btn.italic {
font-weight: normal;
font-style: italic;
font-family: 'Times New Roman', serif;
font-size: 17px;
}
.toolbar-label {
font-size: 11px;
gap: 3px;
padding: 2px 6px;
background: #f8f8f8;
border-radius: 6px;
border: 1px solid #eee;
}
.toolbar-label span {
display: none;
.toolbar-label span:first-child {
display: inline;
color: #888;
}
.toolbar-label input[type="color"] {
width: 24px;
height: 24px;
padding: 1px;
border-radius: 4px;
border: 1px solid #ddd;
cursor: pointer;
}
.toolbar-label select {
max-width: 70px;
font-size: 11px;
padding: 2px 4px;
border-radius: 4px;
border: 1px solid #ddd;
background: #fff;
}
.toolbar-label select option {
font-size: 12px;
}
.weight-slider {
width: 56px;
accent-color: #4a90e2;
}
#weightValue {
min-width: 20px;
text-align: center;
font-size: 11px;
font-weight: bold;
color: #333;
}
.roller-picker {
min-width: 50px;
height: 32px;
padding: 2px 4px;
min-width: 44px;
height: 28px;
padding: 1px 4px;
border-radius: 6px;
border: 1px solid #ddd;
background: #fff;
}
.roller-value {
font-size: 13px;
min-width: 20px;
font-weight: bold;
}
.roller-arrow {
font-size: 8px;
padding: 1px 3px;
}
/* 背景图工具栏仅两项,保持单行 */
.bg-tools {
flex-wrap: nowrap;
}
/* ===== 顶部工具栏 移动端优化 ===== */
.toolbar {
padding: 6px 8px;
gap: 2px;
}
.btn {
min-width: 44px;
height: 28px;
padding: 0 5px;
font-size: 11px;
border-radius: 4px;
}
.btn-primary {
min-width: 60px;
height: 28px;
padding: 0 8px;
font-size: 12px;
}
.toolbar-btn-icon {
width: 22px;
height: 22px;
font-size: 11px;
}
.toolbar-action-group {
gap: 1px;
}
.toolbar-divider-v {
height: 16px;
margin: 0 1px;
}
}
@@ -156,7 +338,8 @@ html, body {
display: none;
justify-content: center;
align-items: center;
gap: 4px;
gap: 2px;
width: 100%;
}
.toolbar-group.active {
@@ -190,17 +373,24 @@ html, body {
font-style: italic;
}
/* 镜像按钮样式 */
.toolbar-btn.mirror-btn {
width: 30px;
height: 30px;
font-size: 16px;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: #ddd;
margin: 0 2px;
margin: 0 1px;
}
.toolbar-label {
display: flex;
align-items: center;
gap: 4px;
gap: 2px;
font-size: 12px;
color: #666;
}
@@ -230,6 +420,36 @@ html, body {
margin: 0;
}
.rotate-slider {
width: 100px;
height: 32px;
cursor: pointer;
accent-color: #4a90e2;
margin: 0;
}
.weight-slider {
width: 80px;
height: 32px;
cursor: pointer;
accent-color: #4a90e2;
margin: 0;
}
/* 旋转度数显示固定宽度 */
#rotateValue {
display: inline-block;
min-width: 40px;
text-align: center;
}
/* 百分比显示固定宽度 */
#opacityValue, #bgScaleValue, #bgOpacityValue {
display: inline-block;
min-width: 40px;
text-align: center;
}
/* 字号滚轮选择器 */
.roller-picker {
display: flex;
@@ -494,27 +714,6 @@ html, body {
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;
@@ -590,3 +789,223 @@ html, body {
.grid-lines.visible {
display: block;
}
/* ==================== 图层面板样式 ==================== */
.layers-panel {
position: fixed;
top: 60px;
right: -280px;
width: 260px;
height: calc(100vh - 80px);
background: #fff;
border-left: 1px solid #e0e0e0;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
z-index: 200;
display: flex;
flex-direction: column;
transition: right 0.3s ease;
}
.layers-panel.active {
right: 0;
}
.layers-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
background: #f8f8f8;
}
.layers-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.layers-close {
width: 24px;
height: 24px;
border: none;
background: transparent;
font-size: 18px;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.layers-close:hover {
background: #e0e0e0;
color: #333;
}
.layers-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.layer-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 4px;
background: #f5f5f5;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.layer-item:hover {
background: #e8e8e8;
}
.layer-item.selected {
background: #e3f2fd;
border-color: #4a90e2;
}
.layer-item.hidden {
opacity: 0.5;
display: flex !important;
}
.layer-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
font-size: 14px;
}
.layer-info {
flex: 1;
min-width: 0;
}
.layer-name {
font-size: 13px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.layer-type {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.layer-visibility {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
background: #4CAF50;
color: white;
border: 2px solid transparent;
}
.layer-visibility.is-hidden {
background: #f44336;
color: white;
}
.layer-visibility:hover {
opacity: 0.8;
transform: scale(1.1);
}
.layer-visibility:active {
transform: scale(0.95);
}
.layers-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 12px;
border-top: 1px solid #e0e0e0;
background: #f8f8f8;
flex-wrap: wrap;
}
.layers-btn {
width: 32px;
height: 32px;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.layers-btn:hover {
background: #f0f0f0;
border-color: #ccc;
}
.layers-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.layers-btn-danger:hover {
background: #ffebee;
border-color: #ef5350;
}
.layers-divider {
width: 1px;
height: 24px;
background: #ddd;
margin: 0 4px;
}
/* 图层隐藏状态(用于画布上的元素) */
.layer-hidden {
visibility: hidden !important;
pointer-events: none !important;
}
/* 移动端适配 */
@media (max-width: 768px) {
.layers-panel {
width: 240px;
right: -260px;
top: 50px;
height: calc(100vh - 50px);
}
.layers-actions {
padding: 8px;
}
.layers-btn {
width: 28px;
height: 28px;
font-size: 12px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 633 KiB

View File

@@ -4,7 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>极简邀请函制作</title>
<link rel="stylesheet" href="./css/style.css">
<link rel="stylesheet" href="./css/style.css?v=20260515">
<link rel="stylesheet" href="./css/qr.css?v=20260515">
</head>
<body>
<div class="app">
@@ -13,6 +14,13 @@
<button class="btn" id="addImageBtn">背景图</button>
<button class="btn" id="addOverlayBtn">层叠图</button>
<button class="btn" id="addTextBtn">文本</button>
<div class="toolbar-divider-v divider-left"></div>
<div class="toolbar-action-group">
<button class="toolbar-btn-icon" id="undoBtn" title="撤销 (Ctrl+Z)">↩</button>
<button class="toolbar-btn-icon" id="redoBtn" title="重做 (Ctrl+Y)">↪</button>
</div>
<div class="toolbar-divider-v divider-right"></div>
<button class="btn" id="layersBtn">图层</button>
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
</div>
@@ -22,7 +30,12 @@
<div class="toolbar-group text-tools" id="textTools">
<button class="toolbar-btn" id="lockBtn" title="锁定位置">🔒</button>
<div class="toolbar-divider"></div>
<button class="toolbar-btn" id="boldBtn" title="加粗">B</button>
<label class="toolbar-label">
<span>粗细</span>
<input type="range" id="weightSlider" min="400" max="900" step="100" value="700" class="weight-slider">
<span id="weightValue">700</span>
</label>
<div class="toolbar-divider"></div>
<button class="toolbar-btn italic" id="italicBtn" title="斜体">I</button>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
@@ -57,20 +70,42 @@
<input type="range" id="opacitySlider" min="0" max="100" value="100" class="opacity-slider">
<span id="opacityValue">100%</span>
</label>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
<span>旋转</span>
<input type="range" id="rotateSlider" min="-180" max="180" value="0" class="rotate-slider">
<span id="rotateValue">0°</span>
</label>
<div class="toolbar-divider"></div>
<button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button>
</div>
<!-- 二维码编辑工具 -->
<div class="toolbar-group qr-tools" id="qrTools">
<label class="toolbar-label">
<span>透明度</span>
<input type="range" id="qrOpacitySlider" min="0" max="100" value="100" class="opacity-slider">
<span id="qrOpacityValue">100%</span>
</label>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
<span>缩放</span>
<input type="range" id="qrScaleSlider" min="0" max="200" value="100" class="scale-slider">
<span id="qrScaleValue">100%</span>
</label>
</div>
<!-- 背景图编辑工具 -->
<div class="toolbar-group bg-tools" id="bgTools">
<label class="toolbar-label">
<span>缩放</span>
<input type="range" id="bgScaleSlider" min="100" max="300" value="100" class="scale-slider">
<span id="bgScaleValue">100%</span>
</label>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
<span>透明度</span>
<input type="range" id="bgOpacitySlider" min="0" max="100" value="100" class="opacity-slider">
<span id="bgOpacityValue">100%</span>
</label>
<div class="toolbar-divider"></div>
<label class="toolbar-label">
<span>缩放</span>
<input type="range" id="bgScaleSlider" min="100" max="300" value="100" class="scale-slider">
<span id="bgScaleValue">100%</span>
</label>
</div>
</div>
@@ -91,24 +126,58 @@
<!-- 输入框图层 -->
<div class="preview-input-layer" id="previewInputLayer">
<!-- 文字输入框1 - 企业 -->
<div class="text-input hidden" id="input1" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 234.445px;"></div>
<div class="text-input hidden" id="input1" contenteditable="false"></div>
<!-- 文字输入框2 - 姓名 -->
<div class="text-input hidden" id="input2" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 276.355px;"></div>
<div class="text-input hidden" id="input2" contenteditable="false"></div>
<!-- 文字输入框3 - 编号 -->
<div class="text-input hidden" id="input3" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 319.048px;"></div>
<div class="text-input hidden" id="input3" contenteditable="false"></div>
</div>
<!-- 提示文字(最上层,层叠图之下) -->
<div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div>
</div>
</div>
<!-- 长按提示 -->
<div class="long-press-tip" id="longPressTip">长按屏幕可保存图片</div>
<!-- 图层面板 -->
<div class="layers-panel" id="layersPanel">
<div class="layers-header">
<span class="layers-title">图层</span>
<button class="layers-close" id="layersClose">×</button>
</div>
<div class="layers-list" id="layersList">
<!-- 图层项将动态生成 -->
</div>
<div class="layers-actions">
<button class="layers-btn" id="layerToggleVisible" title="显示/隐藏">👁</button>
</div>
</div>
</div>
<input type="file" id="fileInput" accept="image/*" hidden>
<canvas id="exportCanvas" hidden></canvas>
<script src="./js/script.js"></script>
<!-- 二维码功能 -->
<button id="qr-btn" title="添加二维码">二维码</button>
<div id="qr-modal">
<div class="qr-box">
<div class="qr-header">
<h3>生成二维码</h3>
<button class="qr-close">&times;</button>
</div>
<div class="qr-body">
<input type="text" id="qr-input" placeholder="输入网址或文字" maxlength="200">
<div id="qr-preview"></div>
</div>
<div class="qr-footer">
<button class="qr-btn qr-cancel">取消</button>
<button class="qr-btn qr-ok">添加到画布</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="./js/config.js?v=20260515"></script>
<script src="./js/script.js?v=20260515"></script>
<script src="./js/qr.js?v=20260515"></script>
</body>
</html>

70
js/config.js Normal file
View File

@@ -0,0 +1,70 @@
// ============================================
// 邀请函制作工具 - 全局配置文件
// 修改以下数值即可调整外观,无需改动代码
// ============================================
//
// 多模板支持:通过 URL 查询参数 ?t=模板名 切换
// 例如:index.html?t=wedding
// 不传参数时使用 default 模板
var CONFIG = {
// 默认模板名(URL 未指定或找不到时使用)
defaultTemplate: 'default',
// ========== 所有模板 ==========
templates: {
// -------- 默认模板 --------
default: {
title: '通用邀请函',
bgImage: './images/default-bg.jpg',
inputs: [
{
id: "input1",
label: "企业",
left: 100,
top: 218.4,
fontWeight: 700,
placeholder: "请输入",
isLocked: false
},
{
id: "input2",
label: "嘉宾",
left: 100,
top: 261.4,
fontWeight: 700,
placeholder: "请输入",
isLocked: false
},
{
id: "input3",
label: "编号",
left: 100,
top: 308.1,
fontWeight: 700,
placeholder: "请输入",
isLocked: false
}
]
}
// -------- 在此下方添加更多模板 --------
// 用法:浏览器打开 index.html?t=wedding
//
,wedding: {
title: '婚礼邀请函',
bgImage: './images/wedding-bg.jpg',
inputs: [
{ id: "input1", label: "新郎", left: 50, top: 200, fontWeight: 700, placeholder: "请输入新郎名" },
{ id: "input2", label: "新娘", left: 50, top: 250, fontWeight: 700, placeholder: "请输入新娘名" },
{ id: "input3", label: "婚期", left: 50, top: 300, fontWeight: 700, placeholder: "请输入日期" }
]
}
}
};
// -------- 兼容旧代码(由 script.js 内部使用) --------
// 以下变量由 script.js 自动从 CONFIG 读取,无需手动修改
var ACTIVE_TEMPLATE = null;

451
js/qr.js Normal file
View File

@@ -0,0 +1,451 @@
// 二维码功能 - 优化版编辑器工具栏
(function() {
'use strict';
// ========== 配置 ==========
const CONFIG = {
MIN_SIZE: 0,
MAX_SIZE: 200,
DEFAULT_SIZE: 100,
PREVIEW_SIZE: 128,
DEBOUNCE_DELAY: 300
};
// ========== DOM 元素缓存 ==========
const elements = {
btn: document.getElementById('qr-btn'),
modal: document.getElementById('qr-modal'),
input: document.getElementById('qr-input'),
preview: document.getElementById('qr-preview'),
closeBtn: document.querySelector('.qr-close'),
cancelBtn: document.querySelector('.qr-cancel'),
okBtn: document.querySelector('.qr-ok'),
// 工具栏
editorToolbar: document.getElementById('editorToolbar'),
textTools: document.getElementById('textTools'),
imageTools: document.getElementById('imageTools'),
bgTools: document.getElementById('bgTools'),
qrTools: document.getElementById('qrTools'),
qrOpacitySlider: document.getElementById('qrOpacitySlider'),
qrOpacityValue: document.getElementById('qrOpacityValue'),
qrScaleSlider: document.getElementById('qrScaleSlider'),
qrScaleValue: document.getElementById('qrScaleValue')
};
// 验证必需元素
if (!elements.btn || !elements.modal) return;
// ========== 状态管理 ==========
const state = {
currentQR: null,
selectedQR: null,
previewTimer: null,
isDragging: false,
qrCounter: 0
};
// ========== 核心功能 ==========
/**
* 检查背景图并显示/隐藏二维码按钮
*/
function checkBackground() {
const container = document.getElementById('imageContainer');
if (!container) return;
const bgImg = container.querySelector('img:not([data-type="qrcode"])');
const hasBackground = bgImg && bgImg.src && !bgImg.src.endsWith(window.location.href);
elements.btn.classList.toggle('show', hasBackground);
}
/**
* 打开二维码生成弹窗
*/
function openModal() {
elements.modal.classList.add('active');
elements.input.value = '';
elements.preview.innerHTML = '';
state.currentQR = null;
elements.input.focus();
}
/**
* 关闭二维码生成弹窗
*/
function closeModal() {
elements.modal.classList.remove('active');
}
/**
* 生成二维码预览
*/
function generatePreview() {
const text = elements.input.value.trim();
if (!text) {
elements.preview.innerHTML = '';
state.currentQR = null;
return;
}
elements.preview.innerHTML = '';
try {
state.currentQR = new QRCode(elements.preview, {
text: text,
width: CONFIG.PREVIEW_SIZE,
height: CONFIG.PREVIEW_SIZE,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
} catch (error) {
console.error('二维码生成失败:', error);
}
}
/**
* 将二维码添加到画布
*/
function addToCanvas() {
if (!state.currentQR) {
alert('请输入内容');
return;
}
const canvas = elements.preview.querySelector('canvas');
if (!canvas) return;
const container = document.getElementById('imageContainer');
const rect = container.getBoundingClientRect();
// 生成图层 ID
state.qrCounter++;
const layerId = `qr_${state.qrCounter}`;
// 创建二维码容器
const wrapper = document.createElement('div');
wrapper.className = 'qr-item';
wrapper.dataset.type = 'qrcode';
wrapper.dataset.layerId = layerId;
wrapper.style.cssText = `
position: absolute;
width: ${CONFIG.DEFAULT_SIZE}px;
height: ${CONFIG.DEFAULT_SIZE}px;
left: ${(rect.width / 2 - CONFIG.DEFAULT_SIZE / 2)}px;
top: ${(rect.height / 2 - CONFIG.DEFAULT_SIZE / 2)}px;
`;
// 创建二维码图片
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.style.cssText = `
width: 100%;
height: 100%;
opacity: 1;
display: block;
`;
// 创建删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.className = 'qr-delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.title = '删除二维码';
deleteBtn.type = 'button';
// 删除按钮点击事件
deleteBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
// 删除二维码元素
wrapper.remove();
// 清除选中状态
if (state.selectedQR === wrapper) {
state.selectedQR = null;
hideQRTools();
if (elements.editorToolbar) {
elements.editorToolbar.classList.remove('active');
}
}
// 保存状态
saveState();
// 更新图层列表
updateLayersList();
return false;
};
wrapper.appendChild(img);
wrapper.appendChild(deleteBtn);
container.appendChild(wrapper);
makeDraggable(wrapper);
selectQR(wrapper);
closeModal();
saveState();
// 更新图层列表
updateLayersList();
}
/**
* 选中二维码
*/
function selectQR(img) {
// 取消其他选中状态
document.querySelectorAll('.qr-item').forEach(el => {
el.classList.remove('qr-selected');
});
// 选中当前二维码
img.classList.add('qr-selected');
state.selectedQR = img;
// 显示二维码工具栏
showQRTools();
}
/**
* 显示二维码工具栏
*/
function showQRTools() {
if (!state.selectedQR) return;
// 激活编辑器工具栏
if (elements.editorToolbar) {
elements.editorToolbar.classList.add('active');
}
// 隐藏其他工具栏
['textTools', 'imageTools', 'bgTools'].forEach(key => {
if (elements[key]) elements[key].classList.remove('active');
});
// 显示二维码工具栏
if (elements.qrTools) {
elements.qrTools.classList.add('active');
}
// 同步滑块值
syncToolbarValues();
}
/**
* 同步工具栏滑块值
*/
function syncToolbarValues() {
if (!state.selectedQR) return;
const img = state.selectedQR.querySelector('img');
const opacity = img ? (parseFloat(img.style.opacity) || 1) : 1;
const size = parseInt(state.selectedQR.style.width) || CONFIG.DEFAULT_SIZE;
if (elements.qrOpacitySlider) {
elements.qrOpacitySlider.value = Math.round(opacity * 100);
elements.qrOpacityValue.textContent = Math.round(opacity * 100) + '%';
}
if (elements.qrScaleSlider) {
elements.qrScaleSlider.value = size;
elements.qrScaleValue.textContent = size + '%';
}
}
/**
* 隐藏二维码工具栏
*/
function hideQRTools() {
if (elements.qrTools) {
elements.qrTools.classList.remove('active');
}
document.querySelectorAll('.qr-item').forEach(el => {
el.classList.remove('qr-selected');
});
state.selectedQR = null;
}
/**
* 更新二维码透明度
*/
function updateOpacity(value) {
if (!state.selectedQR) return;
const img = state.selectedQR.querySelector('img');
if (img) {
img.style.opacity = value / 100;
}
elements.qrOpacityValue.textContent = value + '%';
}
/**
* 更新二维码大小
*/
function updateScale(value) {
if (!state.selectedQR) return;
state.selectedQR.style.width = value + 'px';
state.selectedQR.style.height = value + 'px';
elements.qrScaleValue.textContent = value + '%';
}
/**
* 保存状态
*/
function saveState() {
if (typeof window.saveState === 'function') {
window.saveState();
}
}
/**
* 更新图层列表
*/
function updateLayersList() {
// 触发图层更新事件
const event = new CustomEvent('updateLayers');
document.dispatchEvent(event);
}
// ========== 拖拽功能 ==========
/**
* 使元素可拖拽
*/
function makeDraggable(el) {
let isDragging = false;
let startX, startY, startLeft, startTop;
function startDrag(clientX, clientY) {
isDragging = true;
startX = clientX;
startY = clientY;
startLeft = parseInt(el.style.left) || 0;
startTop = parseInt(el.style.top) || 0;
el.style.cursor = 'grabbing';
}
function moveDrag(clientX, clientY) {
if (!isDragging) return;
el.style.left = (startLeft + clientX - startX) + 'px';
el.style.top = (startTop + clientY - startY) + 'px';
}
function endDrag() {
if (isDragging) {
isDragging = false;
el.style.cursor = 'move';
saveState();
}
}
// 鼠标事件 - 排除删除按钮
el.addEventListener('mousedown', (e) => {
// 如果点击的是删除按钮,不处理拖拽
if (e.target.closest('.qr-delete-btn')) {
return;
}
e.preventDefault();
e.stopPropagation();
selectQR(el);
startDrag(e.clientX, e.clientY);
});
// 触摸事件 - 排除删除按钮
el.addEventListener('touchstart', (e) => {
// 如果触摸的是删除按钮,不处理拖拽
if (e.target.closest('.qr-delete-btn')) {
return;
}
e.preventDefault();
e.stopPropagation();
selectQR(el);
if (e.touches.length === 1) {
startDrag(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
// 全局移动事件
document.addEventListener('mousemove', (e) => moveDrag(e.clientX, e.clientY));
document.addEventListener('touchmove', (e) => {
if (isDragging && e.touches.length === 1) {
e.preventDefault();
moveDrag(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
// 全局结束事件
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
}
// ========== 事件绑定 ==========
function bindEvents() {
// 弹窗事件
elements.btn.addEventListener('click', openModal);
elements.closeBtn.addEventListener('click', closeModal);
elements.cancelBtn.addEventListener('click', closeModal);
elements.okBtn.addEventListener('click', addToCanvas);
elements.modal.addEventListener('click', (e) => {
if (e.target === elements.modal) closeModal();
});
// 输入防抖
elements.input.addEventListener('input', () => {
clearTimeout(state.previewTimer);
state.previewTimer = setTimeout(generatePreview, CONFIG.DEBOUNCE_DELAY);
});
// 透明度滑块
if (elements.qrOpacitySlider) {
elements.qrOpacitySlider.addEventListener('input', (e) => {
updateOpacity(e.target.value);
});
elements.qrOpacitySlider.addEventListener('change', saveState);
}
// 缩放滑块
if (elements.qrScaleSlider) {
elements.qrScaleSlider.addEventListener('input', (e) => {
updateScale(e.target.value);
});
elements.qrScaleSlider.addEventListener('change', saveState);
}
}
// ========== 初始化 ==========
function init() {
// 监听背景图变化
const container = document.getElementById('imageContainer');
if (container) {
const observer = new MutationObserver(checkBackground);
observer.observe(container, { childList: true, subtree: true });
}
// 定期检查背景图状态
setInterval(checkBackground, 500);
// 绑定事件
bindEvents();
// 初始检查
checkBackground();
// 暴露全局 API
window.QRCodeAPI = {
hideTools: hideQRTools,
getSelected: () => state.selectedQR
};
}
// 启动
init();
})();

File diff suppressed because it is too large Load Diff

196
使用说明.md Normal file
View File

@@ -0,0 +1,196 @@
# 邀请函制作工具 - 使用与配置说明
> 以下操作,只需要用记事本打开文件、修改几个数字即可。
---
## 一、调整三个输入框的位置
### 适用场景
打开页面后,输入框(企业、嘉宾、编号)的位置不满意,想上下左右调一调。
### 操作步骤
1. 用 **记事本** 打开 `js/config.js` 文件
2. 找到下半部分的 `INPUTS_CONFIG` 配置段:
```js
var INPUTS_CONFIG = {
"inputs": [
{
"id": "input1", // ← 第一个输入框(企业)
"left": 100, // ← 距离左边多少像素
"top": 218.4 // ← 距离顶部多少像素
},
{
"id": "input2", // ← 第二个输入框(嘉宾)
"left": 100,
"top": 261.4
},
{
"id": "input3", // ← 第三个输入框(编号)
"left": 100,
"top": 308.1
}
]
};
```
3. **修改数字即可**:
- **`left`**:数字越大,输入框越靠右;数字越小,越靠左
- **`top`**:数字越大,输入框越靠下;数字越小,越靠上
> 小技巧:在页面上选中输入框,会显示它的坐标,复制数字填进来即可。
4. **保存文件**,然后刷新页面,效果就会生效。
---
## 二、更换默认背景图片
### 适用场景
打开页面时自动显示的那张背景图想换成自己的图片。
### 操作步骤
**方法一(推荐)—— 替换图片文件:**
1. 准备一张你想要的图片(建议用 JPG 格式)
2. 把图片放到 `images` 文件夹里,重命名为 `default-bg.jpg`
> 如果原来有一张,会提示"替换文件",点击确定即可。
3. 刷新页面,新背景图就会自动加载
**方法二—— 修改配置文件路径:**
1. 把你的图片放到 `images` 文件夹,比如命名为 `my-bg.png`
2. 用记事本打开 `js/config.js`
3. 找到文件开头的这一行:
```js
var DEFAULT_BG_IMAGE = './images/default-bg.jpg';
```
4. 改成你的图片路径:
```js
var DEFAULT_BG_IMAGE = './images/my-bg.png';
```
5. 保存文件,刷新页面
---
## 三、修改输入框的文字粗细
### 适用场景
想让输入框的文字更粗或更细。
### 操作步骤
1. 用记事本打开 `js/config.js`
2. 找到 `"fontWeight": 700` 这一行
3. 修改数字(范围 400 ~ 900):
| 数值 | 效果 |
|------|------|
| 400 | 正常粗细 |
| 500 | 中等 |
| 600 | 半粗 |
| **700** | **加粗(默认)** |
| 800 | 更粗 |
| 900 | 最粗 |
4. 保存文件,刷新页面
---
## 四、修改输入框的占位提示文字
### 适用场景
想把"请输入"改成其他提示语。
### 操作步骤
1. 用记事本打开 `js/config.js`
2. 找到相应的 `"placeholder"` 行,例如:
```js
"placeholder": "请输入"
```
3. 改成你想要的文字,比如:
```js
"placeholder": "在此输入企业名称"
```
4. 保存文件,刷新页面
---
---
## 五、部署多个邀请函模板(通过网址切换)
### 适用场景
部署一个网站,但能做出多个不同样式的邀请函,每个有不同的背景图、输入框位置和提示文字。通过网址后缀来区分。
### 实现原理
在 `js/config.js` 中提前定义好多个模板,通过 URL 查询参数 `?t=模板名` 来切换。
### 操作步骤
1. 用记事本打开 `js/config.js`
2. 找到模板定义区域,已经有一个 `default` 模板,在它后面添加新模板(注意去掉注释标记):
```js
// 在 default 模板的 } 后面加逗号,然后添加:
,wedding: {
title: '婚礼邀请函',
bgImage: './images/wedding-bg.jpg',
inputs: [
{ id: "input1", label: "新郎", left: 50, top: 200, fontWeight: 700, placeholder: "请输入新郎名", isLocked: false },
{ id: "input2", label: "新娘", left: 50, top: 250, fontWeight: 700, placeholder: "请输入新娘名", isLocked: false },
{ id: "input3", label: "婚期", left: 50, top: 300, fontWeight: 700, placeholder: "请输入日期", isLocked: false }
]
}
```
3. 把对应的背景图放到 `images` 文件夹(如 `wedding-bg.jpg`)
4. 部署后,用不同网址访问即可得到不同的邀请函:
| 网址 | 效果 |
|------|------|
| `https://你的域名/index.html` | 默认模板 |
| `https://你的域名/index.html?t=wedding` | 婚礼邀请函模板 |
> 每个模板的输入框位置、提示文字、背景图完全独立,互不干扰。用户制作时修改的内容(文字、样式调整)也不会影响其他模板。
---
## 常见问题
**Q:修改文件后页面没变化?**
A:先确认文件已保存,然后刷新浏览器页面(按 F5 或点浏览器的刷新按钮)。
**Q:改坏了怎么办?**
A:可以重新下载项目,或者联系开发者恢复。
**Q:找不到 `images` 文件夹?**
A:项目文件夹里如果没看到,自己新建一个名叫 `images` 的文件夹即可。
**Q:`config.js` 在哪里?**
A:在 `js` 文件夹里,打开后一眼就能看到全部配置项。