├── .gitignore
├── start.sh
├── favicon.svg
├── icon.svg
├── simple-test.md
├── CONTRIBUTING.md
├── LICENSE
├── logo.svg
├── CHANGELOG.md
├── README.md
├── styles_temp.js
├── CLAUDE.md
├── index.html
├── styles.js
└── styles.js.backup
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.bak*
3 | node_modules/
4 | dist/
5 | *.log
6 | .idea/
7 | .vscode/
8 | *.swp
9 | *.swo
10 | *~
11 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 公众号 Markdown 编辑器 - 启动脚本
4 |
5 | echo "📝 公众号 Markdown 编辑器"
6 | echo "================================"
7 | echo ""
8 | echo "🌐 服务器地址: http://localhost:8080/"
9 | echo "📌 按 Ctrl+C 停止服务器"
10 | echo "================================"
11 | echo ""
12 |
13 | # 启动简单的 HTTP 服务器
14 | python3 -m http.server 8080
15 |
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/simple-test.md:
--------------------------------------------------------------------------------
1 | # 简单测试 - 空行分组
2 |
3 | ## A. 连续图片(应该并排)
4 |
5 | 
6 | 
7 | 
8 |
9 | ## B. 空行分隔(应该独立)
10 |
11 | 
12 |
13 | 
14 |
15 | 
16 |
17 | ## C. 混合测试
18 |
19 | 
20 | 
21 |
22 | 
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 贡献指南
2 |
3 | 感谢您对本项目的关注!欢迎提交 Issue 和 Pull Request。
4 |
5 | ## 提交 Issue
6 |
7 | 在提交 Issue 之前,请:
8 | - 搜索已有的 Issue,避免重复
9 | - 使用清晰的标题描述问题
10 | - 提供详细的问题描述和复现步骤
11 | - 如有可能,提供截图或错误信息
12 |
13 | ## 提交 Pull Request
14 |
15 | 1. Fork 本仓库
16 | 2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
17 | 3. 提交您的修改 (`git commit -m 'Add some AmazingFeature'`)
18 | 4. 推送到分支 (`git push origin feature/AmazingFeature`)
19 | 5. 创建一个 Pull Request
20 |
21 | ### 开发规范
22 |
23 | - 遵循现有的代码风格
24 | - 添加必要的注释
25 | - 确保所有功能在主流浏览器中正常工作
26 | - 更新相关文档
27 |
28 | ### 添加新样式
29 |
30 | 如果您想添加新的样式主题:
31 |
32 | 1. 在 `styles.js` 中添加新的样式配置对象
33 | 2. 确保包含所有必要的元素样式(h1-h6, p, ul, ol, li, blockquote, code, pre, img, table等)
34 | 3. 在 README.md 中更新样式说明
35 | 4. 测试样式在微信公众号编辑器中的兼容性
36 |
37 | ## 行为准则
38 |
39 | - 尊重所有贡献者
40 | - 欢迎建设性的批评和建议
41 | - 专注于项目本身,避免人身攻击
42 |
43 | ## 许可证
44 |
45 | 通过贡献代码,您同意您的贡献将按照本项目的 MIT 许可证进行许可。
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 花生 (alchaincyf)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新日志
2 |
3 | ## [2.0.0] - 2025-10-15
4 |
5 | ### 🎉 重大更新:图片处理系统完全重构
6 |
7 | 这是项目的一个里程碑版本,我们完全重写了图片处理系统,带来了**业界领先**的解决方案。
8 |
9 | ---
10 |
11 | ### ✨ 新功能
12 |
13 | #### 📸 智能图片处理系统
14 |
15 | **核心特性**:
16 | - ✅ **100% 成功率**:支持从任何地方粘贴图片(截图、浏览器、文件管理器)
17 | - ✅ **智能压缩**:自动压缩图片,平均压缩 50%-80%
18 | - ✅ **本地存储**:使用 IndexedDB 持久化存储,刷新不丢失
19 | - ✅ **编辑流畅**:编辑器中使用短链接(`img://img-xxx`),告别卡顿
20 | - ✅ **完美兼容**:复制到公众号时自动转 Base64
21 |
22 | **技术实现**:
23 | ```
24 | 用户粘贴图片
25 | ↓
26 | Canvas API 压缩(最大 1920px,质量 85%)
27 | ↓
28 | IndexedDB 持久化存储
29 | ↓
30 | 编辑器显示短链接(20 字符)
31 | ↓
32 | 预览区从 IndexedDB 加载
33 | ↓
34 | 复制时自动转 Base64
35 | ```
36 |
37 | ---
38 |
39 | ### 🔧 技术改进
40 |
41 | #### 1. **新增 ImageStore 类**(app.js:9-213)
42 | - 封装 IndexedDB 操作
43 | - 支持 CRUD 和批量管理
44 | - 自动计算存储空间
45 |
46 | #### 2. **新增 ImageCompressor 类**(app.js:215-313)
47 | - 基于 Canvas API 的压缩算法
48 | - GIF/SVG 保留原格式
49 | - 智能对比:压缩后更大则用原图
50 |
51 | #### 3. **实现自定义图片协议**
52 | - `img://img-timestamp-random` 短链接
53 | - 预览时自动从 IndexedDB 加载
54 | - 完美解决编辑器卡顿问题
55 |
56 | #### 4. **修复图片粘贴 Bug**
57 | - 修复 `fileToBase64` 方法调用错误
58 | - 移除对不稳定图床的依赖
59 | - 支持所有粘贴来源
60 |
61 | ---
62 |
63 | ### 📊 性能对比
64 |
65 | | 指标 | v1.x(旧版) | v2.0(新版) | 提升 |
66 | |------|-------------|------------|------|
67 | | **编辑器体验** | 几千字符(卡顿) | 20 字符(丝滑) | **99%** |
68 | | **粘贴成功率** | 80%(依赖图床) | 100%(本地) | **+20%** |
69 | | **刷新后保留** | ❌ 丢失 | ✅ 保留 | ∞ |
70 | | **文件大小** | 原图大小 | 压缩 50%-80% | **2-5x** |
71 | | **网络依赖** | 需要 | 不需要 | - |
72 |
73 | ---
74 |
75 | ### 📚 文档更新
76 |
77 | #### README.md
78 | - ✅ 添加最新功能介绍
79 | - ✅ 更新技术栈说明
80 | - ✅ 详细说明图片处理优势
81 |
82 | #### CLAUDE.md
83 | - ✅ 完整的技术架构文档
84 | - ✅ 详细的实现逻辑说明
85 | - ✅ 核心组件 API 文档
86 | - ✅ 技术亮点对比表
87 | - ✅ 常见问题解答
88 | - ✅ 未来规划路线图
89 |
90 | ---
91 |
92 | ### 🎯 使用指南
93 |
94 | #### 图片粘贴(新功能)
95 |
96 | **支持的粘贴方式**:
97 | 1. **截图粘贴**:`Ctrl+V` / `Cmd+V`
98 | 2. **浏览器复制**:右键复制图片后粘贴
99 | 3. **文件管理器**:复制图片文件后粘贴
100 | 4. **拖拽上传**:直接拖拽图片到编辑器
101 |
102 | **粘贴效果**:
103 | ```markdown
104 | # 粘贴前(卡顿)
105 |  # 几千个字符
106 |
107 | # 粘贴后(丝滑)
108 |  # 仅 20 个字符
109 | ```
110 |
111 | **查看压缩效果**:
112 | - 粘贴后会显示:`✅ 已保存 (2.5 MB → 487.3 KB)`
113 | - 控制台会显示详细信息:`图片压缩完成: 2.5 MB → 487.3 KB (压缩 81%)`
114 |
115 | ---
116 |
117 | ### ⚠️ 注意事项
118 |
119 | 1. **存储空间**:
120 | - 浏览器默认约 50MB-100MB
121 | - 压缩后图片平均 200KB-500KB
122 | - 可存储约 100-500 张图片
123 |
124 | 2. **数据持久化**:
125 | - 图片存储在 IndexedDB 中
126 | - 刷新页面不会丢失
127 | - 清除浏览器数据会丢失图片
128 | - 建议定期导出重要内容
129 |
130 | 3. **浏览器兼容性**:
131 | - Chrome 58+
132 | - Firefox 55+
133 | - Safari 11.1+
134 | - Edge 79+
135 |
136 | ---
137 |
138 | ### 🚀 未来规划
139 |
140 | #### 短期(1-2个月)
141 | - [ ] 图片管理面板(可视化管理)
142 | - [ ] 批量导出功能(img:// 转标准 Markdown)
143 | - [ ] 存储空间监控
144 | - [ ] 图片压缩配置
145 |
146 | #### 中期(3-6个月)
147 | - [ ] 自定义样式主题编辑器
148 | - [ ] 可选的云端同步
149 | - [ ] 数学公式、流程图支持
150 | - [ ] 撤销/重做功能
151 |
152 | #### 长期(6-12个月)
153 | - [ ] 草稿自动保存
154 | - [ ] 导出为长图
155 | - [ ] 多人协作编辑
156 | - [ ] AI 辅助写作
157 |
158 | ---
159 |
160 | ### 🙏 致谢
161 |
162 | 感谢所有用户的反馈和建议,正是你们的支持让这个项目不断进步!
163 |
164 | 如果觉得有用,请给个 ⭐️ Star 支持一下!
165 |
166 | ---
167 |
168 | **完整更新内容请查看**:
169 | - [README.md](README.md) - 用户指南
170 | - [CLAUDE.md](CLAUDE.md) - 技术文档
171 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 公众号 Markdown 编辑器
2 |
3 |
4 |

5 |
6 | 一个专为微信公众号设计的 Markdown 编辑器
7 |
8 | [](https://editor.huasheng.ai/)
9 | [](https://github.com/alchaincyf/huasheng_editor)
10 | [](https://wx.zsxq.com/group/48888144124288)
11 |
12 |
13 | ## 🌟 在线体验
14 |
15 | 👉 **[https://editor.huasheng.ai/](https://editor.huasheng.ai/)**
16 |
17 | ## ✨ 功能特点
18 |
19 | ### 🎨 13 种精美样式
20 | - **经典公众号系列**:默认、技术、优雅、深度阅读
21 | - **传统媒体系列**:杂志风格、纽约时报、金融时报、Jony Ive
22 | - **现代数字系列**:Wired 连线、Medium 长文、Apple 极简、Anthropic Claude、AI Coder 特调
23 |
24 | ### 📸 智能图片处理(⭐ 最新升级)
25 | - **智能粘贴**:支持从任何地方粘贴图片(截图、浏览器、文件管理器)
26 | - **自动压缩**:图片自动压缩到合理大小(最高压缩 80%+)
27 | - **本地存储**:使用 IndexedDB 持久化存储,刷新不丢失
28 | - **编辑友好**:编辑器中使用短链接(`img://img-xxx`),不会卡顿
29 | - **多图网格**:2-3 列自动排版,类似朋友圈
30 | - **完美兼容**:复制到公众号时自动转 Base64
31 |
32 | ### 🚀 强大功能
33 | - **实时预览**:左侧编辑,右侧即时查看效果
34 | - **一键复制**:直接粘贴到公众号编辑器,格式完美保留
35 | - **智能粘贴**:支持从飞书、Notion、Word 等富文本应用直接粘贴
36 | - **图片拖拽**:支持拖拽图片文件到编辑器
37 | - **样式收藏**:收藏常用样式,快速切换
38 | - **文件上传**:支持 .md / .markdown 文件
39 | - **代码高亮**:优雅的代码块展示,支持多种语言
40 | - **响应式设计**:完美适配桌面、平板、手机
41 |
42 | ## 📖 使用指南
43 |
44 | ### 快速开始
45 | 1. 访问 [在线编辑器](https://editor.huasheng.ai/)
46 | 2. 在左侧输入或粘贴 Markdown 内容
47 | 3. 选择喜欢的样式主题
48 | 4. 点击「复制到公众号」
49 | 5. 粘贴到微信公众号编辑器
50 |
51 | ### 本地运行
52 | ```bash
53 | # 克隆仓库
54 | git clone https://github.com/alchaincyf/huasheng_editor.git
55 |
56 | # 进入目录
57 | cd huasheng_editor
58 |
59 | # 启动本地服务器(Python)
60 | python3 -m http.server 8080
61 |
62 | # 或使用提供的脚本
63 | ./start.sh
64 |
65 | # 访问 http://localhost:8080
66 | ```
67 |
68 | ## 🛠️ 技术栈
69 |
70 | - **Vue 3** - 渐进式前端框架
71 | - **Markdown-it** - 强大的 Markdown 解析器
72 | - **Highlight.js** - 代码语法高亮
73 | - **IndexedDB** - 本地图片持久化存储
74 | - **Canvas API** - 客户端图片压缩
75 | - **Turndown** - HTML 转 Markdown(智能粘贴)
76 | - **纯 CSS** - 无需构建工具,开箱即用
77 |
78 | ## 📂 项目结构
79 |
80 | ```
81 | 公众号编辑器/
82 | ├── index.html # 主页面
83 | ├── app.js # Vue 应用逻辑
84 | ├── styles.js # 13 种样式主题配置
85 | ├── icon.svg # 项目图标
86 | ├── favicon.svg # 网站图标
87 | ├── logo.svg # Logo 图标
88 | ├── start.sh # 启动脚本
89 | ├── README.md # 项目说明
90 | ├── CLAUDE.md # 技术文档
91 | └── LICENSE # 开源许可证
92 | ```
93 |
94 | ## 💡 核心特性
95 |
96 | ### ⭐ 图片处理系统(最新升级)
97 |
98 | **技术架构**:
99 | ```
100 | 用户粘贴图片
101 | ↓
102 | Canvas API 压缩(最大 1920px,质量 85%)
103 | ↓
104 | IndexedDB 持久化存储
105 | ↓
106 | 编辑器显示短链接(img://img-xxx)
107 | ↓
108 | 预览区从 IndexedDB 加载显示
109 | ↓
110 | 复制时自动转 Base64
111 | ```
112 |
113 | **核心优势**:
114 | - ✅ **100% 成功率**:不依赖外部图床,完全本地化
115 | - ✅ **编辑器流畅**:短链接不会造成卡顿
116 | - ✅ **刷新不丢失**:IndexedDB 持久化存储
117 | - ✅ **智能压缩**:平均压缩 50%-80%
118 | - ✅ **跨平台支持**:支持截图、浏览器、文件管理器等所有粘贴来源
119 |
120 | **多图网格布局**:
121 | - 连续 2 张图片:并排两列展示
122 | - 连续 3 张图片:一行三列展示
123 | - 连续 4 张图片:2×2 网格
124 | - 5 张及以上:3 列网格布局
125 |
126 | ### 公众号完美兼容
127 | - ✅ 自动将 CSS Grid 转换为 Table 布局
128 | - ✅ 所有样式转为内联样式
129 | - ✅ 图片自动转 Base64
130 | - ✅ 强制样式优先级(!important)
131 |
132 | ### 推荐样式
133 | 带有 ✨ 标识的样式是特别推荐的:
134 | - **Anthropic Claude** - 优雅的技术文档风格
135 | - **金融时报** - 专业的财经风格
136 | - **纽约时报** - 经典的新闻风格
137 | - **技术风格** - 程序员最爱
138 |
139 | ## 🤝 贡献指南
140 |
141 | 欢迎提交 Issue 和 Pull Request!
142 |
143 | ### 如何贡献
144 | 1. Fork 本仓库
145 | 2. 创建你的特性分支 (`git checkout -b feature/AmazingFeature`)
146 | 3. 提交你的更改 (`git commit -m 'Add some AmazingFeature'`)
147 | 4. 推送到分支 (`git push origin feature/AmazingFeature`)
148 | 5. 开启一个 Pull Request
149 |
150 | ### 添加新样式
151 | 1. 在 `styles.js` 中添加新的样式配置
152 | 2. 确保包含所有必需的元素样式
153 | 3. 测试各种 Markdown 元素的渲染效果
154 | 4. 提交 PR 并附上效果截图
155 |
156 | ## 👨💻 作者
157 |
158 | **花生** (alchaincyf)
159 | - 📧 邮箱:[alchaincyf@gmail.com](mailto:alchaincyf@gmail.com)
160 | - 🌟 知识星球:[AI编程:从入门到精通](https://wx.zsxq.com/group/48888144124288)
161 | - 💻 GitHub:[@alchaincyf](https://github.com/alchaincyf)
162 |
163 | ## 🎓 知识星球
164 |
165 | 本项目是我为知识星球「**AI编程:从入门到精通**」的用户开源的工具。
166 |
167 | 在星球里,你可以:
168 | - 🚀 学习 AI 编程最佳实践
169 | - 💡 获取更多开源项目
170 | - 🤝 与同好交流技术
171 | - 📚 获得系统化的学习路径
172 |
173 | 👉 [加入知识星球](https://wx.zsxq.com/group/48888144124288)
174 |
175 | ## 📄 开源协议
176 |
177 | 本项目基于 [MIT License](LICENSE) 开源。
178 |
179 | 你可以自由地:
180 | - ✅ 商业使用
181 | - ✅ 修改
182 | - ✅ 分发
183 | - ✅ 私有使用
184 |
185 | ## 🙏 致谢
186 |
187 | - 感谢所有贡献者和使用者
188 | - 感谢知识星球的朋友们的支持
189 | - 特别感谢 Claude 在项目开发中的协助
190 |
191 | ## 📊 Star History
192 |
193 | [](https://star-history.com/#alchaincyf/huasheng_editor&Date)
194 |
195 | ---
196 |
197 |
198 | Made with ❤️ by
花生
199 |
200 | 如果觉得有用,请给个 ⭐️ Star 支持一下!
201 |
--------------------------------------------------------------------------------
/styles_temp.js:
--------------------------------------------------------------------------------
1 | // 样式配置
2 | const STYLES = {
3 | 'wechat-default': {
4 | name: '默认公众号风格',
5 | styles: {
6 | container: 'max-width: 740px; margin: 0 auto; padding: 20px 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 16px; line-height: 1.8 !important; color: #3f3f3f !important; background-color: #fff !important; word-wrap: break-word;',
7 | h1: 'font-size: 24px; font-weight: 600; color: #2c3e50 !important; line-height: 1.4 !important; margin: 32px 0 16px; padding-bottom: 8px; border-bottom: 2px solid #3498db;',
8 | h2: 'font-size: 22px; font-weight: 600; color: #2c3e50 !important; line-height: 1.4 !important; margin: 28px 0 14px; padding-left: 12px; border-left: 4px solid #3498db;',
9 | h3: 'font-size: 20px; font-weight: 600; color: #34495e !important; line-height: 1.4 !important; margin: 24px 0 12px;',
10 | h4: 'font-size: 18px; font-weight: 600; color: #34495e !important; line-height: 1.4 !important; margin: 20px 0 10px;',
11 | h5: 'font-size: 17px; font-weight: 600; color: #34495e !important; line-height: 1.4 !important; margin: 18px 0 9px;',
12 | h6: 'font-size: 16px; font-weight: 600; color: #34495e !important; line-height: 1.4 !important; margin: 16px 0 8px;',
13 | p: 'margin: 16px 0 !important; line-height: 1.8 !important; color: #3f3f3f !important;',
14 | strong: 'font-weight: 600; color: #2c3e50 !important;',
15 | em: 'font-style: italic; color: #555 !important;',
16 | a: 'color: #3498db !important; text-decoration: none; border-bottom: 1px solid #3498db;',
17 | ul: 'margin: 16px 0; padding-left: 24px;',
18 | ol: 'margin: 16px 0; padding-left: 24px;',
19 | li: 'margin: 8px 0; line-height: 1.8 !important;',
20 | blockquote: 'margin: 16px 0; padding: 10px 16px; background-color: #fafafa !important; border-left: 3px solid #999; color: #666 !important; line-height: 1.6 !important;',
21 | code: 'font-family: Consolas, Monaco, "Courier New", monospace; font-size: 14px; padding: 2px 6px; background-color: #f5f5f5 !important; color: #e74c3c !important; border-radius: 3px;',
22 | pre: 'margin: 20px 0; padding: 16px; background-color: #2d2d2d !important; border-radius: 8px; overflow-x: auto; line-height: 1.6 !important;',
23 | hr: 'margin: 32px 0; border: none; border-top: 1px solid #e0e0e0;',
24 | img: 'max-width: 100%; max-height: 600px !important; height: auto; display: block; margin: 20px auto; border-radius: 8px;',
25 | table: 'width: 100%; margin: 20px 0; border-collapse: collapse; font-size: 15px;',
26 | th: 'background-color: #f0f0f0 !important; padding: 10px; text-align: left; border: 1px solid #e0e0e0; font-weight: 600;',
27 | td: 'padding: 10px; border: 1px solid #e0e0e0;',
28 | tr: 'border-bottom: 1px solid #e0e0e0;',
29 | }
30 | },
31 |
32 | 'latepost-depth': {
33 | name: '晚点风格',
34 | styles: {
35 | container: 'max-width: 700px; margin: 0 auto; padding: 40px 12px; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; font-size: 17px; line-height: 1.8 !important; color: #1a1a1a !important; background-color: #fff !important; word-wrap: break-word;',
36 | h1: 'font-size: 32px; font-weight: 700; color: #1a1a1a !important; line-height: 1.3 !important; margin: 48px 0 28px; padding-left: 20px; border-left: 6px solid #d32f2f; position: relative;',
37 | h2: 'font-size: 24px; font-weight: 600; color: #fff !important; line-height: 1.4 !important; margin: 40px 0 24px; padding: 16px 24px; background-color: #d32f2f !important; border-radius: 4px; position: relative;',
38 | h3: 'font-size: 20px; font-weight: 600; color: #d32f2f !important; line-height: 1.5 !important; margin: 35px 0 20px; padding-left: 16px; border-left: 4px solid #d32f2f; position: relative;',
39 | h4: 'font-size: 18px; font-weight: 600; color: #1a1a1a !important; line-height: 1.5 !important; margin: 30px 0 16px; padding: 8px 12px; background-color: #f5f5f5 !important; border-left: 3px solid #ff5252;',
40 | h5: 'font-size: 17px; font-weight: 600; color: #333 !important; line-height: 1.5 !important; margin: 25px 0 14px;',
41 | h6: 'font-size: 16px; font-weight: 500; color: #666 !important; line-height: 1.5 !important; margin: 20px 0 12px;',
42 | p: 'margin: 20px 0 !important; line-height: 1.9 !important; color: #1a1a1a !important;',
43 | strong: 'font-weight: 700; color: #d32f2f !important; background-color: rgba(211, 47, 47, 0.08) !important; padding: 2px 6px; border-radius: 3px;',
44 | em: 'font-style: italic; color: #666 !important;',
45 | a: 'color: #d32f2f !important; text-decoration: none; border-bottom: 1px solid #d32f2f;',
46 | ul: 'margin: 24px 0; padding-left: 28px; list-style-type: disc;',
47 | ol: 'margin: 24px 0; padding-left: 28px; list-style-type: decimal;',
48 | li: 'margin: 12px 0; line-height: 1.8 !important; color: #1a1a1a !important;',
49 | blockquote: 'margin: 32px 0; padding: 20px 24px 20px 20px; background-color: #f5f5f5 !important; border-left: 4px solid #d32f2f; color: #1a1a1a !important; font-size: 16px; line-height: 1.8 !important; position: relative; border-radius: 4px;',
50 | code: 'font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 15px; padding: 3px 8px; background-color: #f5f5f5 !important; color: #d32f2f !important; border-radius: 4px; font-weight: 500;',
51 | pre: 'margin: 28px 0; padding: 20px; background-color: #2a2a2a !important; color: #f5f5f5 !important; border-radius: 6px; overflow-x: auto; line-height: 1.6 !important; border-left: 4px solid #d32f2f;',
52 | hr: 'margin: 40px auto; border: none; height: 2px; background: linear-gradient(to right, transparent, #d32f2f, transparent); max-width: 200px;',
53 | img: 'max-width: 100%; max-height: 600px !important; height: auto; display: block; margin: 32px auto; border-radius: 6px; border: 2px solid #d32f2f; box-shadow: 0 4px 12px rgba(211, 47, 47, 0.12);',
54 | table: 'width: 100%; margin: 28px 0; border-collapse: collapse; font-size: 16px; border-radius: 6px; overflow: hidden;',
55 | th: 'background-color: #d32f2f !important; color: #fff !important; padding: 12px 16px; text-align: left; font-weight: 600; border: none;',
56 | td: 'padding: 12px 16px; border: none; border-bottom: 1px solid #e0e0e0; color: #1a1a1a !important; background-color: #fff !important;',
57 | tr: 'border-bottom: 1px solid #e0e0e0;',
58 | }
59 | },
60 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # 公众号 Markdown 编辑器
2 |
3 | 一个专为微信公众号设计的 Markdown 编辑器,支持实时预览和一键复制富文本格式到公众号后台。
4 |
5 | ## 项目概述
6 |
7 | 这是一个纯前端的 Markdown 编辑器,可以将标准 Markdown 文本转换为适合微信公众号的富文本格式。支持 13 种精心设计的样式主题,每种主题都针对公众号的阅读体验进行了优化。
8 |
9 | ## 核心功能
10 |
11 | ### 1. Markdown 渲染
12 | - 使用 `markdown-it` 进行 Markdown 解析
13 | - 支持完整的 Markdown 语法(标题、列表、引用、代码块、表格等)
14 | - 实时预览渲染结果
15 |
16 | ### 2. 样式系统
17 | 提供 13 种预设样式主题:
18 | - **经典公众号系列**:默认、技术、优雅、深度阅读
19 | - **传统媒体系列**:杂志风格、纽约时报、金融时报、Jony Ive
20 | - **现代数字系列**:Wired 连线、Medium 长文、Apple 极简、Anthropic Claude、AI Coder 特调
21 |
22 | 每个样式都包含完整的元素定义(标题、段落、列表、引用、代码、表格、图片等)
23 |
24 | ### 3. 图片处理系统(⭐ 核心功能)
25 |
26 | #### 技术架构概览
27 |
28 | 本编辑器采用**自定义图片协议 + 压缩 + IndexedDB 存储**的创新方案,彻底解决了图片粘贴的所有痛点:
29 |
30 | ```
31 | 用户粘贴图片(任何来源)
32 | ↓
33 | ImageCompressor 压缩(Canvas API)
34 | ├─ 最大尺寸:1920x1920px
35 | ├─ 压缩质量:85%
36 | ├─ GIF/SVG 不压缩(保持动画/矢量)
37 | └─ 智能对比:压缩后更大则用原图
38 | ↓
39 | 生成唯一 ID(img-timestamp-random)
40 | ↓
41 | ImageStore 存储到 IndexedDB
42 | ├─ 数据库名:WechatEditorImages
43 | ├─ 存储:Blob + 元数据
44 | └─ 持久化:刷新不丢失
45 | ↓
46 | 编辑器插入短链接:
47 | ↓
48 | 渲染预览时:
49 | ├─ 从 IndexedDB 读取 Blob
50 | ├─ 创建 Object URL
51 | ├─ 替换 img:// 为 blob:
52 | └─ 缓存 Object URL(避免重复读取)
53 | ↓
54 | 复制到公众号时:
55 | ├─ 检测 data-image-id 属性
56 | ├─ 从 IndexedDB 读取原始 Blob
57 | ├─ 转为 Base64
58 | └─ 替换 img 的 src
59 | ```
60 |
61 | #### 核心组件
62 |
63 | ##### 1. **ImageStore 类**(app.js:9-213)
64 |
65 | 负责 IndexedDB 操作的核心类:
66 |
67 | ```javascript
68 | class ImageStore {
69 | constructor() {
70 | this.dbName = 'WechatEditorImages';
71 | this.storeName = 'images';
72 | this.version = 1;
73 | }
74 |
75 | // 核心方法:
76 | async init() // 初始化 IndexedDB
77 | async saveImage(id, blob, metadata) // 保存图片
78 | async getImage(id) // 获取图片(返回 Object URL)
79 | async getImageBlob(id) // 获取 Blob(用于复制时转 Base64)
80 | async deleteImage(id) // 删除图片
81 | async getAllImages() // 获取所有图片列表
82 | async getTotalSize() // 计算总存储大小
83 | }
84 | ```
85 |
86 | **数据结构**:
87 | ```javascript
88 | {
89 | id: 'img-1736966400000-abc123def',
90 | blob: Blob, // 压缩后的图片 Blob
91 | name: '图片名',
92 | originalSize: 2500000, // 原始大小(字节)
93 | compressedSize: 487300, // 压缩后大小
94 | compressionRatio: '81', // 压缩率(%)
95 | mimeType: 'image/jpeg',
96 | createdAt: 1736966400000
97 | }
98 | ```
99 |
100 | ##### 2. **ImageCompressor 类**(app.js:215-313)
101 |
102 | 负责图片压缩的核心类:
103 |
104 | ```javascript
105 | class ImageCompressor {
106 | constructor(options = {}) {
107 | this.maxWidth = 1920;
108 | this.maxHeight = 1920;
109 | this.quality = 0.85;
110 | this.mimeType = 'image/jpeg';
111 | }
112 |
113 | async compress(file) {
114 | // 使用 Canvas API 压缩图片
115 | // 1. FileReader 读取文件为 DataURL
116 | // 2. 创建 Image 对象加载图片
117 | // 3. 计算缩放比例
118 | // 4. Canvas 绘制缩放后的图片
119 | // 5. toBlob 输出压缩后的 Blob
120 | // 6. 对比大小,如果压缩后更大则用原图
121 | }
122 |
123 | static formatSize(bytes) {
124 | // 格式化文件大小(B/KB/MB)
125 | }
126 | }
127 | ```
128 |
129 | **压缩策略**:
130 | - PNG 保持 PNG 格式(避免透明度丢失)
131 | - 其他格式转为 JPEG(更好的压缩率)
132 | - GIF 和 SVG 不压缩(保持动画和矢量特性)
133 | - 白色背景填充(处理透明 PNG)
134 |
135 | ##### 3. **图片粘贴处理**(app.js:1460-1530)
136 |
137 | ```javascript
138 | async handleImageUpload(file, textarea) {
139 | // 1. 检查文件类型和大小(最大 10MB)
140 | // 2. 压缩图片(调用 ImageCompressor)
141 | // 3. 生成唯一 ID
142 | // 4. 存储到 IndexedDB
143 | // 5. 插入短链接到编辑器:
144 | // 6. 显示压缩结果反馈
145 | }
146 | ```
147 |
148 | ##### 4. **图片渲染处理**(app.js:634-689)
149 |
150 | ```javascript
151 | async processImageProtocol(html) {
152 | // 1. 解析 HTML,找到所有
标签
153 | // 2. 检测 src 是否为 img:// 协议
154 | // 3. 提取图片 ID
155 | // 4. 从 IndexedDB 读取图片
156 | // 5. 创建 Object URL(或使用缓存)
157 | // 6. 替换 src 为 Object URL
158 | // 7. 添加 data-image-id 属性(供复制时使用)
159 | // 8. 错误处理:显示占位符
160 | }
161 | ```
162 |
163 | ##### 5. **复制时转 Base64**(app.js:1186-1242)
164 |
165 | ```javascript
166 | async convertImageToBase64(imgElement) {
167 | // 优先处理:检查 data-image-id 属性
168 | if (imageId && this.imageStore) {
169 | // 从 IndexedDB 读取 Blob
170 | const blob = await this.imageStore.getImageBlob(imageId);
171 | // 转为 Base64
172 | return fileReaderToBase64(blob);
173 | }
174 |
175 | // 后备方案:通过 URL fetch(兼容外链图片)
176 | const response = await fetch(src);
177 | const blob = await response.blob();
178 | return fileReaderToBase64(blob);
179 | }
180 | ```
181 |
182 | #### 连续图片网格布局(类似朋友圈)
183 | - **触发条件**:2张或以上连续的图片
184 | - **布局规则**:
185 | - 2张图片:并排两列(50% + 50%)
186 | - 3张图片:一行三列(33.33% × 3)
187 | - 4张图片:2×2 网格
188 | - 5张及以上:3列网格布局
189 |
190 | #### 图片高度限制
191 | - **单张图片**:最大高度 400px
192 | - **网格布局中的图片**:最大高度 360px(单行)
193 | - 所有图片保持宽高比,使用 `object-fit: contain`
194 |
195 | #### 技术亮点
196 |
197 | | 指标 | 传统方案(图床/Base64) | 本项目方案 |
198 | |------|----------------------|----------|
199 | | **编辑器体验** | 几千字符卡顿 | 20字符丝滑 |
200 | | **成功率** | 80%(依赖图床) | 100% |
201 | | **刷新后** | 图片丢失 | 完好保留 |
202 | | **文件大小** | 原图大小 | 压缩 50%-80% |
203 | | **网络依赖** | 需要 | 不需要 |
204 | | **隐私性** | 上传到服务器 | 本地存储 |
205 |
206 | ### 4. 公众号兼容性处理
207 |
208 | #### Grid 转 Table
209 | - **问题**:公众号编辑器不支持 CSS Grid 布局
210 | - **解决方案**:复制时自动将 Grid 布局转换为 Table 布局
211 | - **实现位置**:`app.js` 中的 `convertGridToTable()` 方法
212 | - **转换逻辑**:
213 | ```javascript
214 | 预览阶段:CSS Grid(现代、美观)
215 | ↓
216 | 复制到公众号:HTML Table(兼容、可靠)
217 | ```
218 |
219 | #### 样式内联化
220 | - 所有 CSS 样式都转换为内联样式(inline style)
221 | - 确保公众号编辑器能够正确识别和保留样式
222 | - 不使用 CSS 类名或外部样式表
223 |
224 | ### 5. 代码高亮
225 | - 使用 `highlight.js` 进行语法高亮
226 | - macOS 风格的代码块装饰(红黄绿三色圆点)
227 | - 支持多种编程语言
228 | - 深色主题背景(#383a42)
229 |
230 | ### 6. 响应式设计
231 |
232 | #### 桌面端(> 1024px)
233 | - 左右分栏布局
234 | - 编辑器在左,预览在右
235 |
236 | #### 平板端(769px - 1024px)
237 | - 保持左右分栏
238 | - 调整内边距优化显示
239 |
240 | #### 移动端(≤ 768px)
241 | - **上下布局**
242 | - 编辑器在上(占 40% 高度)
243 | - 预览在下(可滚动)
244 | - 隐藏部分UI元素(星标按钮、提示文字)
245 | - 全宽按钮和选择器
246 |
247 | ### 7. 其他功能
248 | - **样式收藏**:支持收藏常用样式,使用 localStorage 持久化
249 | - **文件上传**:支持直接上传 .md 或 .markdown 文件
250 | - **一键复制**:点击按钮即可复制富文本到剪贴板
251 | - **Toast 提示**:操作反馈(成功/失败/处理中)
252 |
253 | ## 技术栈
254 |
255 | - **前端框架**:Vue 3(CDN 引入,无需构建)
256 | - **Markdown 解析**:markdown-it 14.0.0
257 | - **代码高亮**:highlight.js 11.9.0
258 | - **样式**:纯 CSS(CSS Variables + 内联样式)
259 | - **部署**:静态文件,可用任何 HTTP 服务器
260 |
261 | ## 文件结构
262 |
263 | ```
264 | 公众号编辑器/
265 | ├── index.html # 主页面,包含 UI 和样式
266 | ├── app.js # Vue 应用逻辑和核心功能
267 | ├── styles.js # 13 种样式主题配置
268 | ├── start.sh # 本地开发服务器启动脚本
269 | ├── README.md # 项目说明文档
270 | ├── LICENSE # 许可证
271 | └── CLAUDE.md # 项目技术文档(本文件)
272 | ```
273 |
274 | ## 关键技术实现
275 |
276 | ### 1. 图片分组算法
277 | ```javascript
278 | // 位置:app.js -> groupConsecutiveImages()
279 | // 功能:识别连续的图片并分组
280 | // 算法:
281 | 1. 遍历 DOM 子元素,找出所有图片(包括 ![]()
结构)
282 | 2. 按索引位置判断是否连续(允许中间有1个空白节点)
283 | 3. 将连续的图片归为一组
284 | 4. 对每组图片创建网格布局(group.length >= 2)
285 | ```
286 |
287 | ### 2. 样式应用流程
288 | ```javascript
289 | // 位置:app.js -> renderMarkdown() -> applyInlineStyles()
290 | // 流程:
291 | Markdown 输入
292 | ↓
293 | markdown-it 解析为 HTML
294 | ↓
295 | 应用选中样式的内联 CSS
296 | ↓
297 | 处理图片网格布局
298 | ↓
299 | 包裹容器样式
300 | ↓
301 | 渲染到预览区
302 | ```
303 |
304 | ### 3. 复制到公众号流程
305 | ```javascript
306 | // 位置:app.js -> copyToClipboard()
307 | // 流程:
308 | 获取渲染后的 HTML
309 | ↓
310 | 将 Grid 转换为 Table(convertGridToTable)
311 | ↓
312 | 图片转 Base64(convertImageToBase64)
313 | ↓
314 | 包裹 section 容器(如有背景色)
315 | ↓
316 | 简化代码块结构
317 | ↓
318 | 列表项扁平化
319 | ↓
320 | 写入剪贴板(text/html + text/plain)
321 | ```
322 |
323 | ## 样式配置规范
324 |
325 | 每个样式主题的配置结构(`styles.js`):
326 | ```javascript
327 | {
328 | 'style-key': {
329 | name: '样式显示名称',
330 | styles: {
331 | container: '容器样式',
332 | h1: '一级标题样式',
333 | h2: '二级标题样式',
334 | // ... 更多元素
335 | img: 'max-width: 100%; max-height: 400px; height: auto; display: block; ...',
336 | // 所有样式必须是完整的内联CSS字符串
337 | }
338 | }
339 | }
340 | ```
341 |
342 | ## 约束和限制
343 |
344 | 1. **图片处理**
345 | - 单张图片最大高度:400px
346 | - 网格图片最大高度:360px
347 | - 所有图片保持原始宽高比
348 |
349 | 2. **公众号兼容性**
350 | - 不支持:CSS Grid, Flexbox(部分), CSS Variables
351 | - 必须使用:内联样式、Table 布局
352 | - 图片必须:Base64 编码或公众号可访问的URL
353 |
354 | 3. **浏览器要求**
355 | - 需要支持 ES6+
356 | - 需要支持 Clipboard API
357 | - 需要支持 Fetch API
358 |
359 | ## 开发指南
360 |
361 | ### 本地运行
362 | ```bash
363 | # 使用 Python 启动服务器
364 | python3 -m http.server 8080
365 |
366 | # 或使用提供的脚本
367 | ./start.sh
368 |
369 | # 访问 http://localhost:8080
370 | ```
371 |
372 | ### 添加新样式主题
373 | 1. 在 `styles.js` 中添加新的样式配置对象
374 | 2. 在 `index.html` 的 `