Compare commits

21 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
10 changed files with 1554 additions and 176 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*

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

@@ -188,39 +188,144 @@ html, body {
z-index: 10000; z-index: 10000;
} }
/* 移动端:允许横向滚动 */ /* 移动端:两行整洁布局 @ 375px 友好 */
@media (max-width: 768px) { @media (max-width: 768px) {
.editor-toolbar { .editor-toolbar {
justify-content: flex-start; justify-content: center;
overflow-x: auto; overflow: visible;
-webkit-overflow-scrolling: touch; padding: 6px 10px;
scrollbar-width: none; height: auto;
padding: 8px; box-shadow: 0 1px 6px rgba(0,0,0,0.06);
}
.editor-toolbar::-webkit-scrollbar {
display: none;
} }
.toolbar-group { .toolbar-group {
gap: 2px; gap: 6px;
flex-shrink: 0; flex-wrap: wrap;
width: 100%;
justify-content: center; justify-content: center;
align-items: center;
width: 100%;
padding: 4px 0;
}
/* 分隔线在折叠布局中隐藏 */
.toolbar-divider {
display: none;
} }
.toolbar-btn { .toolbar-btn {
width: 32px; width: 30px;
height: 32px; height: 30px;
font-size: 14px; 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 { .toolbar-label {
font-size: 12px; font-size: 11px;
gap: 3px;
padding: 2px 6px;
background: #f8f8f8;
border-radius: 6px;
border: 1px solid #eee;
} }
.toolbar-label span:first-child { .toolbar-label span:first-child {
display: inline; 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 { .roller-picker {
min-width: 50px; min-width: 44px;
height: 32px; height: 28px;
padding: 2px 4px; 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;
} }
} }
@@ -323,6 +428,14 @@ html, body {
margin: 0; margin: 0;
} }
.weight-slider {
width: 80px;
height: 32px;
cursor: pointer;
accent-color: #4a90e2;
margin: 0;
}
/* 旋转度数显示固定宽度 */ /* 旋转度数显示固定宽度 */
#rotateValue { #rotateValue {
display: inline-block; display: inline-block;

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 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="./css/style.css"> <link rel="stylesheet" href="./css/style.css?v=20260515">
<link rel="stylesheet" href="./css/qr.css?v=20260515">
</head> </head>
<body> <body>
<div class="app"> <div class="app">
@@ -29,7 +30,12 @@
<div class="toolbar-group text-tools" id="textTools"> <div class="toolbar-group text-tools" id="textTools">
<button class="toolbar-btn" id="lockBtn" title="锁定位置">🔒</button> <button class="toolbar-btn" id="lockBtn" title="锁定位置">🔒</button>
<div class="toolbar-divider"></div> <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> <button class="toolbar-btn italic" id="italicBtn" title="斜体">I</button>
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<label class="toolbar-label"> <label class="toolbar-label">
@@ -73,6 +79,20 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button> <button class="toolbar-btn mirror-btn" id="mirrorHBtn" title="水平镜像">↔</button>
</div> </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"> <div class="toolbar-group bg-tools" id="bgTools">
<label class="toolbar-label"> <label class="toolbar-label">
@@ -106,11 +126,11 @@
<!-- 输入框图层 --> <!-- 输入框图层 -->
<div class="preview-input-layer" id="previewInputLayer"> <div class="preview-input-layer" id="previewInputLayer">
<!-- 文字输入框1 - 企业 --> <!-- 文字输入框1 - 企业 -->
<div class="text-input hidden" id="input1" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 234.445px; font-weight: bold;"></div> <div class="text-input hidden" id="input1" contenteditable="false"></div>
<!-- 文字输入框2 - 姓名 --> <!-- 文字输入框2 - 姓名 -->
<div class="text-input hidden" id="input2" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 276.355px; font-weight: bold;"></div> <div class="text-input hidden" id="input2" contenteditable="false"></div>
<!-- 文字输入框3 - 编号 --> <!-- 文字输入框3 - 编号 -->
<div class="text-input hidden" id="input3" contenteditable="false" data-placeholder="请输入" style="left: 98px; top: 319.048px; font-weight: bold;"></div> <div class="text-input hidden" id="input3" contenteditable="false"></div>
</div> </div>
<!-- 提示文字(最上层,层叠图之下) --> <!-- 提示文字(最上层,层叠图之下) -->
<div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div> <div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div>
@@ -136,6 +156,28 @@
<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="./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> </body>
</html> </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();
})();

1
js/qrcode.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,8 @@
const bgScaleValue = document.getElementById('bgScaleValue'); const bgScaleValue = document.getElementById('bgScaleValue');
const bgOpacitySlider = document.getElementById('bgOpacitySlider'); const bgOpacitySlider = document.getElementById('bgOpacitySlider');
const bgOpacityValue = document.getElementById('bgOpacityValue'); const bgOpacityValue = document.getElementById('bgOpacityValue');
const boldBtn = document.getElementById('boldBtn'); const weightSlider = document.getElementById('weightSlider');
const weightValue = document.getElementById('weightValue');
const italicBtn = document.getElementById('italicBtn'); const italicBtn = document.getElementById('italicBtn');
const lockBtn = document.getElementById('lockBtn'); const lockBtn = document.getElementById('lockBtn');
const colorPicker = document.getElementById('colorPicker'); const colorPicker = document.getElementById('colorPicker');
@@ -44,6 +45,36 @@
const guideV = document.getElementById('guideV'); const guideV = document.getElementById('guideV');
const gridLines = document.getElementById('gridLines'); const gridLines = document.getElementById('gridLines');
var inputsConfig = null;
var defaultBgUrl = './images/default-bg.jpg';
// 解析 URL 查询参数 ?t=模板名,选择对应模板
(function resolveTemplate() {
try {
var params = new URLSearchParams(window.location.search);
var templateName = params.get('t') || (typeof CONFIG !== 'undefined' ? CONFIG.defaultTemplate : 'default');
if (typeof CONFIG !== 'undefined' && CONFIG.templates && CONFIG.templates[templateName]) {
var tmpl = CONFIG.templates[templateName];
ACTIVE_TEMPLATE = tmpl;
inputsConfig = tmpl.inputs || null;
if (tmpl.bgImage) defaultBgUrl = tmpl.bgImage;
} else if (typeof CONFIG !== 'undefined' && CONFIG.templates && CONFIG.templates[CONFIG.defaultTemplate]) {
var tmpl = CONFIG.templates[CONFIG.defaultTemplate];
ACTIVE_TEMPLATE = tmpl;
inputsConfig = tmpl.inputs || null;
if (tmpl.bgImage) defaultBgUrl = tmpl.bgImage;
} else {
// 兼容旧版 INPUTS_CONFIG / DEFAULT_BG_IMAGE
if (typeof INPUTS_CONFIG !== 'undefined') inputsConfig = INPUTS_CONFIG.inputs;
if (typeof DEFAULT_BG_IMAGE !== 'undefined') defaultBgUrl = DEFAULT_BG_IMAGE;
}
} catch (e) {
if (typeof INPUTS_CONFIG !== 'undefined') inputsConfig = INPUTS_CONFIG.inputs;
if (typeof DEFAULT_BG_IMAGE !== 'undefined') defaultBgUrl = DEFAULT_BG_IMAGE;
}
window._inputsConfig = inputsConfig;
})();
let currentImage = null; let currentImage = null;
let bgImageScale = 1; let bgImageScale = 1;
let bgImageOpacity = 1; let bgImageOpacity = 1;
@@ -263,6 +294,23 @@
updateUndoRedoButtons(); updateUndoRedoButtons();
// 从配置文件应用输入框初始参数
if (inputsConfig) {
inputsConfig.forEach(function(cfg) {
var el = document.getElementById(cfg.id);
if (el) {
el.style.left = cfg.left + 'px';
el.style.top = cfg.top + 'px';
if (cfg.fontWeight) el.style.fontWeight = cfg.fontWeight;
if (cfg.placeholder) el.dataset.placeholder = cfg.placeholder;
if (cfg.isLocked) {
el.classList.add('locked');
el.dataset.locked = 'true';
}
}
});
}
// 初始化输入框 // 初始化输入框
initInput(input1); initInput(input1);
initInput(input2); initInput(input2);
@@ -381,9 +429,10 @@
e.target === document.getElementById('emptyTip'); e.target === document.getElementById('emptyTip');
const isOverlayImage = e.target.closest('.overlay-image'); const isOverlayImage = e.target.closest('.overlay-image');
const isTextInput = e.target.closest('.text-input'); const isTextInput = e.target.closest('.text-input');
const isQRCode = e.target.closest('.qr-item');
// 点击背景/空白区域 // 点击背景/空白区域(排除二维码)
if (isBackground && !isOverlayImage) { if (isBackground && !isOverlayImage && !isQRCode) {
// 如果正在编辑,先结束编辑 // 如果正在编辑,先结束编辑
if (editingInput) { if (editingInput) {
endEdit(editingInput); endEdit(editingInput);
@@ -392,24 +441,33 @@
if (selectedInput) { if (selectedInput) {
selectedInput.classList.remove('selected'); selectedInput.classList.remove('selected');
selectedInput = null; selectedInput = null;
hidePositionInfo();
} }
// 取消层叠图选中状态 // 取消层叠图选中状态
if (selectedOverlay) { if (selectedOverlay) {
selectedOverlay.classList.remove('selected'); selectedOverlay.classList.remove('selected');
selectedOverlay = null; selectedOverlay = null;
} }
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
// 隐藏辅助线 // 隐藏辅助线
hideGuides(); hideGuides();
// 隐藏编辑器工具栏 // 隐藏编辑器工具栏
editorToolbar.classList.remove('active'); editorToolbar.classList.remove('active');
} }
// 点击空白区域(不是层叠图和输入框) // 点击空白区域(不是层叠图、输入框和二维码)
if (!isOverlayImage && !isTextInput && !isBackground) { if (!isOverlayImage && !isTextInput && !isBackground && !isQRCode) {
if (selectedOverlay) { if (selectedOverlay) {
selectedOverlay.classList.remove('selected'); selectedOverlay.classList.remove('selected');
selectedOverlay = null; selectedOverlay = null;
} }
// 隐藏二维码工具栏
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
} }
} }
@@ -779,9 +837,9 @@
} }
} }
// 加载默认背景图 // 加载默认背景图(路径从 config.js 根据 ?t= 参数读取)
function loadDefaultBackground() { function loadDefaultBackground() {
const defaultBgUrl = './images/default-bg.jpg'; // defaultBgUrl 已在模板解析阶段确定
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
currentImage = img; currentImage = img;
@@ -1183,6 +1241,7 @@
if (selectedInput) { if (selectedInput) {
selectedInput.classList.remove('selected'); selectedInput.classList.remove('selected');
selectedInput = null; selectedInput = null;
hidePositionInfo();
} }
if (editingInput) { if (editingInput) {
endEdit(editingInput); endEdit(editingInput);
@@ -1198,6 +1257,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.add('active'); imageTools.classList.add('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新透明度滑块为当前层叠图的透明度 // 更新透明度滑块为当前层叠图的透明度
const currentOpacity = overlayEl.style.opacity || '1'; const currentOpacity = overlayEl.style.opacity || '1';
@@ -1362,6 +1424,21 @@
// 点击背景图显示工具栏 // 点击背景图显示工具栏
bgImage.addEventListener('click', (e) => { bgImage.addEventListener('click', (e) => {
if (!currentImage) return; if (!currentImage) return;
// 取消输入框和层叠图的选中状态
if (editingInput) {
endEdit(editingInput);
}
if (selectedInput) {
selectedInput.classList.remove('selected');
selectedInput = null;
hidePositionInfo();
}
if (selectedOverlay) {
selectedOverlay.classList.remove('selected');
selectedOverlay = null;
}
e.stopPropagation(); e.stopPropagation();
isInteractingWithBgImage = true; isInteractingWithBgImage = true;
@@ -1371,6 +1448,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1385,6 +1465,21 @@
bgLayer.addEventListener('click', (e) => { bgLayer.addEventListener('click', (e) => {
if (!currentImage) return; if (!currentImage) return;
// 取消输入框和层叠图的选中状态
if (editingInput) {
endEdit(editingInput);
}
if (selectedInput) {
selectedInput.classList.remove('selected');
selectedInput = null;
hidePositionInfo();
}
if (selectedOverlay) {
selectedOverlay.classList.remove('selected');
selectedOverlay = null;
}
e.stopPropagation(); e.stopPropagation();
isInteractingWithBgImage = true; isInteractingWithBgImage = true;
@@ -1394,6 +1489,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1412,8 +1510,8 @@
return; return;
} }
// 如果正在编辑,先退出编辑状态 // 取消输入框和层叠图的选中状态(在 touchstart 时就处理,不依赖 click 事件)
if (editingInput) { if (editingInput || selectedInput || selectedOverlay) {
deselectAll(); deselectAll();
} }
@@ -1444,6 +1542,9 @@
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 同步滑块值 // 同步滑块值
bgScaleSlider.value = bgImageScale * 100; bgScaleSlider.value = bgImageScale * 100;
@@ -1483,6 +1584,53 @@
isInteractingWithBgImage = false; isInteractingWithBgImage = false;
} }
} }
// ==================== 输入框位置信息显示 ====================
var positionInfoEl = null;
// 创建位置信息提示元素
function ensurePositionInfo() {
if (!positionInfoEl) {
positionInfoEl = document.createElement('div');
positionInfoEl.className = 'input-position-info';
positionInfoEl.style.cssText = 'display:none;position:fixed;padding:2px 6px;background:rgba(0,0,0,0.45);color:#fff;font-size:11px;border-radius:3px;pointer-events:none;z-index:10000;white-space:nowrap;font-family:monospace;';
document.body.appendChild(positionInfoEl);
}
return positionInfoEl;
}
// 显示并更新位置信息
function showPositionInfo(element) {
var info = ensurePositionInfo();
var left = parseFloat(element.style.left) || 0;
var top = parseFloat(element.style.top) || 0;
info.textContent = 'left: ' + left.toFixed(1) + 'px top: ' + top.toFixed(1) + 'px';
// 将 info 定位在输入框上方
var rect = element.getBoundingClientRect();
info.style.left = (rect.left + rect.width / 2 - info.offsetWidth / 2) + 'px';
info.style.top = (rect.top - 30) + 'px';
info.style.display = 'block';
}
// 更新位置信息(拖拽中实时调用)
function updatePositionInfo(element, left, top) {
var info = ensurePositionInfo();
info.textContent = 'left: ' + left.toFixed(1) + 'px top: ' + top.toFixed(1) + 'px';
info.style.left = (parseFloat(info.style.left)) + 'px';
// 重新定位到输入框上方
var rect = element.getBoundingClientRect();
info.style.left = (rect.left + rect.width / 2 - info.offsetWidth / 2) + 'px';
info.style.top = (rect.top - 30) + 'px';
}
// 隐藏位置信息
function hidePositionInfo() {
if (positionInfoEl) {
positionInfoEl.style.display = 'none';
}
}
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态 // 初始化输入框 - 每个输入框独立管理自己的拖拽状态
function initInput(element) { function initInput(element) {
@@ -1612,6 +1760,9 @@
element.style.left = snapped.left + 'px'; element.style.left = snapped.left + 'px';
element.style.top = snapped.top + 'px'; element.style.top = snapped.top + 'px';
// 实时更新位置信息显示
updatePositionInfo(element, snapped.left, snapped.top);
// 显示对齐辅助线 // 显示对齐辅助线
showAlignmentGuides(element); showAlignmentGuides(element);
} }
@@ -1669,8 +1820,10 @@
// 如果已经在编辑中,不重复进入 // 如果已经在编辑中,不重复进入
if (editingInput === element) return; if (editingInput === element) return;
// 选中并立即进入编辑模式 // 阻止事件冒泡,避免触发容器点击
selectInput(element); e.stopPropagation();
// 进入编辑模式(selectInput 已在 mousedown 中调用)
startEdit(element); startEdit(element);
}); });
} }
@@ -1699,9 +1852,15 @@
textTools.classList.add('active'); textTools.classList.add('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 更新工具栏状态 // 更新工具栏状态
updateToolbarState(element); updateToolbarState(element);
// 显示位置信息
showPositionInfo(element);
} }
// 开始编辑 // 开始编辑
@@ -1722,6 +1881,9 @@
textTools.classList.add('active'); textTools.classList.add('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.remove('active'); bgTools.classList.remove('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
// 移动端/编辑模式:固定在顶部始终可见 // 移动端/编辑模式:固定在顶部始终可见
if (window.innerWidth <= 768 || 'ontouchstart' in window) { if (window.innerWidth <= 768 || 'ontouchstart' in window) {
@@ -1801,10 +1963,7 @@
const fontFamily = style.fontFamily.replace(/['"]/g, ''); const fontFamily = style.fontFamily.replace(/['"]/g, '');
const fontSize = parseInt(style.fontSize); const fontSize = parseInt(style.fontSize);
// 更新加粗按钮状态 // 更新颜色选择器
boldBtn.classList.toggle('active', fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 700);
// 更新斜体按钮状态
italicBtn.classList.toggle('active', fontStyle === 'italic'); italicBtn.classList.toggle('active', fontStyle === 'italic');
// 更新颜色选择器 // 更新颜色选择器
@@ -1827,13 +1986,28 @@
fontPicker.value = fontPicker.options[0].value; fontPicker.value = fontPicker.options[0].value;
} }
// 更新字重滑动条(兼容关键字 bold/normal 和数值)
var numericWeight = parseInt(style.fontWeight);
if (!isNaN(numericWeight)) {
weightSlider.value = numericWeight;
weightValue.textContent = numericWeight;
} else if (fontWeight === 'bold') {
weightSlider.value = 700;
weightValue.textContent = '700';
} else {
weightSlider.value = 400;
weightValue.textContent = '400';
}
// 更新锁定按钮状态 // 更新锁定按钮状态
lockBtn.classList.toggle('active', element.classList.contains('locked')); lockBtn.classList.toggle('active', element.classList.contains('locked'));
// 更新字号选择器 // 更新字号选择器
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;
} }
@@ -2243,6 +2417,11 @@
if (selectedInput) { if (selectedInput) {
selectedInput.classList.remove('selected'); selectedInput.classList.remove('selected');
selectedInput = null; selectedInput = null;
hidePositionInfo();
}
if (selectedOverlay) {
selectedOverlay.classList.remove('selected');
selectedOverlay = null;
} }
// 隐藏编辑器工具栏 // 隐藏编辑器工具栏
editorToolbar.classList.remove('active'); editorToolbar.classList.remove('active');
@@ -2269,22 +2448,15 @@
// 初始化文本编辑器工具栏 // 初始化文本编辑器工具栏
function initTextEditorToolbar() { function initTextEditorToolbar() {
// 加粗按钮 // 字重滑动条
boldBtn.addEventListener('click', () => { weightSlider.addEventListener('input', function() {
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (!targetInput) return; if (!target) return;
const selection = window.getSelection(); var val = parseInt(this.value);
if (selection.rangeCount > 0 && !selection.isCollapsed) { target.style.fontWeight = val;
document.execCommand('bold', false, null); weightValue.textContent = val;
} else { });
const isBold = boldBtn.classList.contains('active'); weightSlider.addEventListener('change', function() {
if (isBold) {
targetInput.style.fontWeight = 'normal';
} else {
targetInput.style.fontWeight = 'bold';
}
boldBtn.classList.toggle('active');
}
saveState(); saveState();
}); });
@@ -2352,47 +2524,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) { // 获取当前选中输入框的实际字号
newSize = Math.max(10, Math.min(42, newSize)); function getCurrentFontSize() {
const targetInput = editingInput || selectedInput; var target = editingInput || selectedInput;
if (!target) return fontSizeValue;
if (forceUpdate || targetInput) { var size = parseInt(window.getComputedStyle(target).fontSize);
targetInput.style.fontSize = newSize + 'px'; return isNaN(size) ? 16 : size;
fontSizeValue = newSize;
fontSizeValueEl.textContent = newSize;
} else {
fontSizeValue = newSize;
fontSizeValueEl.textContent = newSize;
}
} }
fontSizeUpBtn.addEventListener('click', () => { function updateFontSize(newSize, forceUpdate) {
const targetInput = editingInput || selectedInput; newSize = Math.max(10, Math.min(42, newSize));
if (targetInput) { var target = editingInput || selectedInput;
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue; if (!target) return;
updateFontSize(currentSize + 1, true); target.style.fontSize = newSize + 'px';
saveState(); fontSizeValue = newSize;
} fontSizeValueEl.textContent = newSize;
}
fontSizeUpBtn.addEventListener('click', function() {
var target = editingInput || selectedInput;
if (!target) return;
var currentSize = parseInt(target.style.fontSize) || parseInt(window.getComputedStyle(target).fontSize) || fontSizeValue;
updateFontSize(currentSize + 1);
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 });
} }
@@ -2407,6 +2579,11 @@
if (!layersBtn || !layersPanel) return; if (!layersBtn || !layersPanel) return;
// 监听二维码图层更新事件
document.addEventListener('updateLayers', () => {
updateLayersList();
});
// 打开/关闭图层面板(同时支持桌面和移动端) // 打开/关闭图层面板(同时支持桌面和移动端)
const toggleLayersPanel = () => { const toggleLayersPanel = () => {
layersPanel.classList.toggle('active'); layersPanel.classList.toggle('active');
@@ -2561,7 +2738,20 @@
} }
}); });
// 4. 层叠图(最上层) // 4. 二维码
const qrItems = document.querySelectorAll('.qr-item');
qrItems.forEach((qr, index) => {
layers.push({
id: qr.dataset.layerId || `qr_${index}`,
element: qr,
name: `二维码 ${index + 1}`,
type: 'qrcode',
icon: '🔲',
visible: !qr.classList.contains('layer-hidden')
});
});
// 5. 层叠图(最上层)
const overlayImages = overlayLayer.querySelectorAll('.overlay-image'); const overlayImages = overlayLayer.querySelectorAll('.overlay-image');
overlayImages.forEach((overlay, index) => { overlayImages.forEach((overlay, index) => {
layers.push({ layers.push({
@@ -2631,7 +2821,8 @@
const names = { const names = {
'text': '文本', 'text': '文本',
'overlay': '层叠图', 'overlay': '层叠图',
'background': '背景图' 'background': '背景图',
'qrcode': '二维码'
}; };
return names[type] || type; return names[type] || type;
} }
@@ -2658,12 +2849,49 @@
selectInput(element); selectInput(element);
} else if (element.classList.contains('overlay-image')) { } else if (element.classList.contains('overlay-image')) {
selectOverlay(element); selectOverlay(element);
} else if (element.classList.contains('qr-item')) {
// 选中二维码
if (window.QRCodeAPI) {
window.QRCodeAPI.hideTools();
}
// 取消其他选中
document.querySelectorAll('.qr-item').forEach(el => {
el.classList.remove('qr-selected');
});
// 选中当前二维码
element.classList.add('qr-selected');
// 显示二维码工具栏
editorToolbar.classList.add('active');
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.remove('active');
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.add('active');
// 同步工具栏值
const qrImg = element.querySelector('img');
const qrOpacitySlider = document.getElementById('qrOpacitySlider');
const qrOpacityValue = document.getElementById('qrOpacityValue');
const qrScaleSlider = document.getElementById('qrScaleSlider');
const qrScaleValue = document.getElementById('qrScaleValue');
if (qrImg && qrOpacitySlider) {
const opacity = parseFloat(qrImg.style.opacity) || 1;
qrOpacitySlider.value = Math.round(opacity * 100);
qrOpacityValue.textContent = Math.round(opacity * 100) + '%';
}
if (qrScaleSlider) {
const size = parseInt(element.style.width) || 100;
qrScaleSlider.value = size;
qrScaleValue.textContent = size + '%';
}
} else if (element.id === 'bgImage') { } else if (element.id === 'bgImage') {
// 选中背景图 // 选中背景图
editorToolbar.classList.add('active'); editorToolbar.classList.add('active');
textTools.classList.remove('active'); textTools.classList.remove('active');
imageTools.classList.remove('active'); imageTools.classList.remove('active');
bgTools.classList.add('active'); bgTools.classList.add('active');
// 隐藏二维码工具栏
const qrTools = document.getElementById('qrTools');
if (qrTools) qrTools.classList.remove('active');
} }
} }
@@ -3172,6 +3400,45 @@
drawTextOnCanvasHD(ctx, textInput, scale); drawTextOnCanvasHD(ctx, textInput, scale);
} }
}); });
// 绘制二维码
const qrWrappers = container.querySelectorAll('.qr-item');
for (const qrWrapper of qrWrappers) {
if (!qrWrapper.classList.contains('layer-hidden')) {
const qrImg = qrWrapper.querySelector('img');
if (!qrImg) continue;
const rect = qrWrapper.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x = (rect.left - containerRect.left) * scale;
const y = (rect.top - containerRect.top) * scale;
const w = rect.width * scale;
const h = rect.height * scale;
// 应用透明度
const opacity = parseFloat(qrImg.style.opacity) || 1;
ctx.save();
ctx.globalAlpha = opacity;
// 确保图片已加载
if (qrImg.complete && qrImg.naturalWidth > 0) {
ctx.drawImage(qrImg, x, y, w, h);
} else {
// 等待图片加载完成
await new Promise(resolve => {
qrImg.onload = () => {
ctx.drawImage(qrImg, x, y, w, h);
resolve();
};
qrImg.onerror = () => resolve();
// 超时处理
setTimeout(resolve, 100);
});
}
ctx.restore();
}
}
// 记录总耗时 // 记录总耗时
const endTime = performance.now(); const endTime = performance.now();
@@ -3473,11 +3740,11 @@
lineHeight = lineHeight * scale; lineHeight = lineHeight * scale;
} }
// 处理字体粗细 - 统一转换为标准格式 // 处理字体粗细 - 直接使用原始数值(canvas 支持 1~1000 的数值权重)
let fontWeight = style.fontWeight; var fontWeight = style.fontWeight;
// 数值转换为关键字 // 仅将 CSS 关键字映射为对应数值
if (fontWeight === '400' || fontWeight === 'normal') fontWeight = 'normal'; if (fontWeight === 'normal') fontWeight = '400';
else if (fontWeight === '700' || fontWeight === 'bold') fontWeight = 'bold'; else if (fontWeight === 'bold') fontWeight = '700';
// 获取斜体样式 // 获取斜体样式
let fontStyle = style.fontStyle; let fontStyle = style.fontStyle;

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` 文件夹里,打开后一眼就能看到全部配置项。