Compare commits
21 Commits
master
...
Branch_mas
| Author | SHA1 | Date | |
|---|---|---|---|
| 55dfeed2c2 | |||
| 9e0a7c083d | |||
| 396559b2ec | |||
| 7c6128dd6d | |||
| caa107ff64 | |||
| 30886888ba | |||
| be00d6e3a9 | |||
| f090f18a9d | |||
| 315f568138 | |||
| 8789a76ecc | |||
| 708a3d5be8 | |||
| c6f7787c35 | |||
|
|
6a0294f0c0 | ||
|
|
4ee95df792 | ||
|
|
c7963cec6e | ||
|
|
37aaec1e42 | ||
|
|
02f3adb936 | ||
|
|
81f63781a9 | ||
|
|
79203d418b | ||
|
|
b380e65161 | ||
|
|
a7d0ad8e51 |
207
README.md
207
README.md
@@ -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,输入框放置在右下角
|
||||
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. 将编辑器工具栏颜色,字体,字号间距缩小一些,美观大方
|
||||
30. 缩小编辑器工具栏间距,适应小屏幕
|
||||
31. 适配微信浏览器优化导出图片质量,优化画布尺寸计算逻辑以保持宽高比
|
||||
32. 限制边界不强制吸附,优化移动端编辑交互逻辑,解决ios设备上键盘无法弹出的问题,优化统一桌面和移动端触摸编辑和键盘触发逻辑
|
||||
33. 导出高清背景图文本的缩放计算逻辑,优化编辑栏固定与交互,改进字体加载和文本渲染逻辑,支持在选中元素上应用样式
|
||||
34. 调整输入框位置,优化字体设置与选择器匹配逻辑,添加输入框字体加粗样式
|
||||
35. 添加层叠图片旋转功能,为旋转滑动条添加0度,90度,-90度吸附功能,数值单独占位
|
||||
36. 层叠图镜像功能,优化编辑器工具栏按钮样式间距,调整背景图缩放和透明度顺序
|
||||
37. 项目添加撤销重做功能,检查修复撤销重做后背景图和文本位置显示异常问题,撤销重做按钮只需要点击一次就能执行操作
|
||||
38. 优化撤销功能,优化顶部编辑栏按钮占位,优化撤销/重做按钮与文本按钮和保存/预览按钮的间距,文本按钮 ↔ [4px] ↔ 分隔线 ↔ 撤销按钮 ↔ [3px] ↔ 重做按钮 ↔ 分隔线 ↔ [4px] ↔ 保存按钮
|
||||
39. 优化顶部工具栏样式,调整撤销重做按钮分隔栏边距,重构层叠图渲染到最终保存图片上的逻辑
|
||||
一款以手机端为主、电脑端兼容的简易邀请函在线制作工具。无需安装、无需后端,打开浏览器即可使用。用户可上传背景图、添加文字/图片/二维码、拖动调整位置,最终一键生成并保存完整邀请函图片。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
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 # 输入框初始化配置(独立配置文件)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 核心功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **背景图** | 上传 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
189
css/qr.css
Normal 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;
|
||||
}
|
||||
149
css/style.css
149
css/style.css
@@ -188,39 +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;
|
||||
width: 100%;
|
||||
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: 12px;
|
||||
font-size: 11px;
|
||||
gap: 3px;
|
||||
padding: 2px 6px;
|
||||
background: #f8f8f8;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +428,14 @@ html, body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.weight-slider {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
accent-color: #4a90e2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 旋转度数显示固定宽度 */
|
||||
#rotateValue {
|
||||
display: inline-block;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 633 KiB |
54
index.html
54
index.html
@@ -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">
|
||||
@@ -29,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">
|
||||
@@ -73,6 +79,20 @@
|
||||
<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">
|
||||
@@ -106,11 +126,11 @@
|
||||
<!-- 输入框图层 -->
|
||||
<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; font-weight: bold;"></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; font-weight: bold;"></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; font-weight: bold;"></div>
|
||||
<div class="text-input hidden" id="input3" contenteditable="false"></div>
|
||||
</div>
|
||||
<!-- 提示文字(最上层,层叠图之下) -->
|
||||
<div class="empty-tip" id="emptyTip">请点击上方「添加图片」按钮上传背景图</div>
|
||||
@@ -136,6 +156,28 @@
|
||||
<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">×</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
70
js/config.js
Normal 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
451
js/qr.js
Normal 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
1
js/qrcode.min.js
vendored
File diff suppressed because one or more lines are too long
413
js/script.js
413
js/script.js
@@ -28,7 +28,8 @@
|
||||
const bgScaleValue = document.getElementById('bgScaleValue');
|
||||
const bgOpacitySlider = document.getElementById('bgOpacitySlider');
|
||||
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 lockBtn = document.getElementById('lockBtn');
|
||||
const colorPicker = document.getElementById('colorPicker');
|
||||
@@ -44,6 +45,36 @@
|
||||
const guideV = document.getElementById('guideV');
|
||||
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 bgImageScale = 1;
|
||||
let bgImageOpacity = 1;
|
||||
@@ -263,6 +294,23 @@
|
||||
|
||||
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(input2);
|
||||
@@ -381,9 +429,10 @@
|
||||
e.target === document.getElementById('emptyTip');
|
||||
const isOverlayImage = e.target.closest('.overlay-image');
|
||||
const isTextInput = e.target.closest('.text-input');
|
||||
const isQRCode = e.target.closest('.qr-item');
|
||||
|
||||
// 点击背景/空白区域
|
||||
if (isBackground && !isOverlayImage) {
|
||||
// 点击背景/空白区域(排除二维码)
|
||||
if (isBackground && !isOverlayImage && !isQRCode) {
|
||||
// 如果正在编辑,先结束编辑
|
||||
if (editingInput) {
|
||||
endEdit(editingInput);
|
||||
@@ -392,24 +441,33 @@
|
||||
if (selectedInput) {
|
||||
selectedInput.classList.remove('selected');
|
||||
selectedInput = null;
|
||||
hidePositionInfo();
|
||||
}
|
||||
// 取消层叠图选中状态
|
||||
if (selectedOverlay) {
|
||||
selectedOverlay.classList.remove('selected');
|
||||
selectedOverlay = null;
|
||||
}
|
||||
// 隐藏二维码工具栏
|
||||
if (window.QRCodeAPI) {
|
||||
window.QRCodeAPI.hideTools();
|
||||
}
|
||||
// 隐藏辅助线
|
||||
hideGuides();
|
||||
// 隐藏编辑器工具栏
|
||||
editorToolbar.classList.remove('active');
|
||||
}
|
||||
|
||||
// 点击空白区域(不是层叠图和输入框)
|
||||
if (!isOverlayImage && !isTextInput && !isBackground) {
|
||||
// 点击空白区域(不是层叠图、输入框和二维码)
|
||||
if (!isOverlayImage && !isTextInput && !isBackground && !isQRCode) {
|
||||
if (selectedOverlay) {
|
||||
selectedOverlay.classList.remove('selected');
|
||||
selectedOverlay = null;
|
||||
}
|
||||
// 隐藏二维码工具栏
|
||||
if (window.QRCodeAPI) {
|
||||
window.QRCodeAPI.hideTools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,9 +837,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认背景图
|
||||
// 加载默认背景图(路径从 config.js 根据 ?t= 参数读取)
|
||||
function loadDefaultBackground() {
|
||||
const defaultBgUrl = './images/default-bg.jpg';
|
||||
// defaultBgUrl 已在模板解析阶段确定
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
currentImage = img;
|
||||
@@ -1183,6 +1241,7 @@
|
||||
if (selectedInput) {
|
||||
selectedInput.classList.remove('selected');
|
||||
selectedInput = null;
|
||||
hidePositionInfo();
|
||||
}
|
||||
if (editingInput) {
|
||||
endEdit(editingInput);
|
||||
@@ -1198,6 +1257,9 @@
|
||||
textTools.classList.remove('active');
|
||||
imageTools.classList.add('active');
|
||||
bgTools.classList.remove('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
|
||||
// 更新透明度滑块为当前层叠图的透明度
|
||||
const currentOpacity = overlayEl.style.opacity || '1';
|
||||
@@ -1362,6 +1424,21 @@
|
||||
// 点击背景图显示工具栏
|
||||
bgImage.addEventListener('click', (e) => {
|
||||
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();
|
||||
|
||||
isInteractingWithBgImage = true;
|
||||
@@ -1371,6 +1448,9 @@
|
||||
textTools.classList.remove('active');
|
||||
imageTools.classList.remove('active');
|
||||
bgTools.classList.add('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
|
||||
// 同步滑块值
|
||||
bgScaleSlider.value = bgImageScale * 100;
|
||||
@@ -1385,6 +1465,21 @@
|
||||
|
||||
bgLayer.addEventListener('click', (e) => {
|
||||
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();
|
||||
|
||||
isInteractingWithBgImage = true;
|
||||
@@ -1394,6 +1489,9 @@
|
||||
textTools.classList.remove('active');
|
||||
imageTools.classList.remove('active');
|
||||
bgTools.classList.add('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
|
||||
// 同步滑块值
|
||||
bgScaleSlider.value = bgImageScale * 100;
|
||||
@@ -1412,8 +1510,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果正在编辑,先退出编辑状态
|
||||
if (editingInput) {
|
||||
// 取消输入框和层叠图的选中状态(在 touchstart 时就处理,不依赖 click 事件)
|
||||
if (editingInput || selectedInput || selectedOverlay) {
|
||||
deselectAll();
|
||||
}
|
||||
|
||||
@@ -1444,6 +1542,9 @@
|
||||
textTools.classList.remove('active');
|
||||
imageTools.classList.remove('active');
|
||||
bgTools.classList.add('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
|
||||
// 同步滑块值
|
||||
bgScaleSlider.value = bgImageScale * 100;
|
||||
@@ -1484,6 +1585,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 输入框位置信息显示 ====================
|
||||
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) {
|
||||
let isMouseDown = false;
|
||||
@@ -1612,6 +1760,9 @@
|
||||
element.style.left = snapped.left + 'px';
|
||||
element.style.top = snapped.top + 'px';
|
||||
|
||||
// 实时更新位置信息显示
|
||||
updatePositionInfo(element, snapped.left, snapped.top);
|
||||
|
||||
// 显示对齐辅助线
|
||||
showAlignmentGuides(element);
|
||||
}
|
||||
@@ -1669,8 +1820,10 @@
|
||||
// 如果已经在编辑中,不重复进入
|
||||
if (editingInput === element) return;
|
||||
|
||||
// 选中并立即进入编辑模式
|
||||
selectInput(element);
|
||||
// 阻止事件冒泡,避免触发容器点击
|
||||
e.stopPropagation();
|
||||
|
||||
// 进入编辑模式(selectInput 已在 mousedown 中调用)
|
||||
startEdit(element);
|
||||
});
|
||||
}
|
||||
@@ -1699,9 +1852,15 @@
|
||||
textTools.classList.add('active');
|
||||
imageTools.classList.remove('active');
|
||||
bgTools.classList.remove('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
|
||||
// 更新工具栏状态
|
||||
updateToolbarState(element);
|
||||
|
||||
// 显示位置信息
|
||||
showPositionInfo(element);
|
||||
}
|
||||
|
||||
// 开始编辑
|
||||
@@ -1722,6 +1881,9 @@
|
||||
textTools.classList.add('active');
|
||||
imageTools.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) {
|
||||
@@ -1801,10 +1963,7 @@
|
||||
const fontFamily = style.fontFamily.replace(/['"]/g, '');
|
||||
const fontSize = parseInt(style.fontSize);
|
||||
|
||||
// 更新加粗按钮状态
|
||||
boldBtn.classList.toggle('active', fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 700);
|
||||
|
||||
// 更新斜体按钮状态
|
||||
// 更新颜色选择器
|
||||
italicBtn.classList.toggle('active', fontStyle === 'italic');
|
||||
|
||||
// 更新颜色选择器
|
||||
@@ -1827,13 +1986,28 @@
|
||||
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'));
|
||||
|
||||
// 更新字号选择器
|
||||
const fontSizeValueEl = fontSizePicker.querySelector('.roller-value');
|
||||
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();
|
||||
fontSizeValue = displaySize;
|
||||
}
|
||||
@@ -2243,6 +2417,11 @@
|
||||
if (selectedInput) {
|
||||
selectedInput.classList.remove('selected');
|
||||
selectedInput = null;
|
||||
hidePositionInfo();
|
||||
}
|
||||
if (selectedOverlay) {
|
||||
selectedOverlay.classList.remove('selected');
|
||||
selectedOverlay = null;
|
||||
}
|
||||
// 隐藏编辑器工具栏
|
||||
editorToolbar.classList.remove('active');
|
||||
@@ -2269,22 +2448,15 @@
|
||||
|
||||
// 初始化文本编辑器工具栏
|
||||
function initTextEditorToolbar() {
|
||||
// 加粗按钮
|
||||
boldBtn.addEventListener('click', () => {
|
||||
const targetInput = editingInput || selectedInput;
|
||||
if (!targetInput) return;
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||||
document.execCommand('bold', false, null);
|
||||
} else {
|
||||
const isBold = boldBtn.classList.contains('active');
|
||||
if (isBold) {
|
||||
targetInput.style.fontWeight = 'normal';
|
||||
} else {
|
||||
targetInput.style.fontWeight = 'bold';
|
||||
}
|
||||
boldBtn.classList.toggle('active');
|
||||
}
|
||||
// 字重滑动条
|
||||
weightSlider.addEventListener('input', function() {
|
||||
var target = editingInput || selectedInput;
|
||||
if (!target) return;
|
||||
var val = parseInt(this.value);
|
||||
target.style.fontWeight = val;
|
||||
weightValue.textContent = val;
|
||||
});
|
||||
weightSlider.addEventListener('change', function() {
|
||||
saveState();
|
||||
});
|
||||
|
||||
@@ -2352,47 +2524,47 @@
|
||||
const fontSizeUpBtn = fontSizeRoller.querySelector('.roller-up');
|
||||
const fontSizeDownBtn = fontSizeRoller.querySelector('.roller-down');
|
||||
|
||||
function updateFontSize(newSize, forceUpdate = false) {
|
||||
newSize = Math.max(10, Math.min(42, newSize));
|
||||
const targetInput = editingInput || selectedInput;
|
||||
|
||||
if (forceUpdate || targetInput) {
|
||||
targetInput.style.fontSize = newSize + 'px';
|
||||
fontSizeValue = newSize;
|
||||
fontSizeValueEl.textContent = newSize;
|
||||
} else {
|
||||
fontSizeValue = newSize;
|
||||
fontSizeValueEl.textContent = newSize;
|
||||
}
|
||||
// 获取当前选中输入框的实际字号
|
||||
function getCurrentFontSize() {
|
||||
var target = editingInput || selectedInput;
|
||||
if (!target) return fontSizeValue;
|
||||
var size = parseInt(window.getComputedStyle(target).fontSize);
|
||||
return isNaN(size) ? 16 : size;
|
||||
}
|
||||
|
||||
fontSizeUpBtn.addEventListener('click', () => {
|
||||
const targetInput = editingInput || selectedInput;
|
||||
if (targetInput) {
|
||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||
updateFontSize(currentSize + 1, true);
|
||||
saveState();
|
||||
}
|
||||
function updateFontSize(newSize, forceUpdate) {
|
||||
newSize = Math.max(10, Math.min(42, newSize));
|
||||
var target = editingInput || selectedInput;
|
||||
if (!target) return;
|
||||
target.style.fontSize = newSize + 'px';
|
||||
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', () => {
|
||||
const targetInput = editingInput || selectedInput;
|
||||
if (targetInput) {
|
||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||
updateFontSize(currentSize - 1, true);
|
||||
saveState();
|
||||
}
|
||||
fontSizeDownBtn.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();
|
||||
});
|
||||
|
||||
fontSizeRoller.addEventListener('wheel', (e) => {
|
||||
fontSizeRoller.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
const targetInput = editingInput || selectedInput;
|
||||
if (targetInput) {
|
||||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||||
const delta = e.deltaY > 0 ? -1 : 1;
|
||||
updateFontSize(currentSize + delta, true);
|
||||
saveState();
|
||||
}
|
||||
var target = editingInput || selectedInput;
|
||||
if (!target) return;
|
||||
var currentSize = parseInt(target.style.fontSize) || parseInt(window.getComputedStyle(target).fontSize) || fontSizeValue;
|
||||
var delta = e.deltaY > 0 ? -1 : 1;
|
||||
updateFontSize(currentSize + delta);
|
||||
saveState();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
@@ -2407,6 +2579,11 @@
|
||||
|
||||
if (!layersBtn || !layersPanel) return;
|
||||
|
||||
// 监听二维码图层更新事件
|
||||
document.addEventListener('updateLayers', () => {
|
||||
updateLayersList();
|
||||
});
|
||||
|
||||
// 打开/关闭图层面板(同时支持桌面和移动端)
|
||||
const toggleLayersPanel = () => {
|
||||
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');
|
||||
overlayImages.forEach((overlay, index) => {
|
||||
layers.push({
|
||||
@@ -2631,7 +2821,8 @@
|
||||
const names = {
|
||||
'text': '文本',
|
||||
'overlay': '层叠图',
|
||||
'background': '背景图'
|
||||
'background': '背景图',
|
||||
'qrcode': '二维码'
|
||||
};
|
||||
return names[type] || type;
|
||||
}
|
||||
@@ -2658,12 +2849,49 @@
|
||||
selectInput(element);
|
||||
} else if (element.classList.contains('overlay-image')) {
|
||||
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') {
|
||||
// 选中背景图
|
||||
editorToolbar.classList.add('active');
|
||||
textTools.classList.remove('active');
|
||||
imageTools.classList.remove('active');
|
||||
bgTools.classList.add('active');
|
||||
// 隐藏二维码工具栏
|
||||
const qrTools = document.getElementById('qrTools');
|
||||
if (qrTools) qrTools.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3173,6 +3401,45 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 绘制二维码
|
||||
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();
|
||||
console.log(`画布生成总耗时: ${(endTime - startTime).toFixed(2)}ms`);
|
||||
@@ -3473,11 +3740,11 @@
|
||||
lineHeight = lineHeight * scale;
|
||||
}
|
||||
|
||||
// 处理字体粗细 - 统一转换为标准格式
|
||||
let fontWeight = style.fontWeight;
|
||||
// 数值转换为关键字
|
||||
if (fontWeight === '400' || fontWeight === 'normal') fontWeight = 'normal';
|
||||
else if (fontWeight === '700' || fontWeight === 'bold') fontWeight = 'bold';
|
||||
// 处理字体粗细 - 直接使用原始数值(canvas 支持 1~1000 的数值权重)
|
||||
var fontWeight = style.fontWeight;
|
||||
// 仅将 CSS 关键字映射为对应数值
|
||||
if (fontWeight === 'normal') fontWeight = '400';
|
||||
else if (fontWeight === 'bold') fontWeight = '700';
|
||||
|
||||
// 获取斜体样式
|
||||
let fontStyle = style.fontStyle;
|
||||
|
||||
196
使用说明.md
Normal file
196
使用说明.md
Normal 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` 文件夹里,打开后一眼就能看到全部配置项。
|
||||
Reference in New Issue
Block a user