mail-editor-pancake 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +498 -218
- package/package.json +2 -2
- package/packages/blocks/dist/index.d.ts +12 -0
- package/packages/blocks/dist/index.js +187 -145
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/src/html.ts +11 -3
- package/packages/blocks/src/index.ts +2 -0
- package/packages/blocks/src/social.ts +8 -11
- package/packages/blocks/src/socialGroup.ts +8 -11
- package/packages/blocks/src/socialShared.ts +52 -0
- package/packages/blocks/src/text.ts +8 -3
- package/packages/core/dist/index.d.ts +275 -12
- package/packages/core/dist/index.js +4099 -2194
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/editor/BlockCodeModal.ts +84 -2
- package/packages/core/src/editor/Canvas.ts +191 -26
- package/packages/core/src/editor/Editor.ts +589 -76
- package/packages/core/src/editor/ExportModal.ts +1 -1
- package/packages/core/src/editor/ImportDocModal.ts +129 -0
- package/packages/core/src/editor/InlineEditor.ts +246 -22
- package/packages/core/src/editor/LeftPanel.ts +55 -14
- package/packages/core/src/editor/Modal.ts +20 -1
- package/packages/core/src/editor/PreviewModal.ts +2 -2
- package/packages/core/src/editor/RichTextToolbar.ts +125 -18
- package/packages/core/src/editor/RightPanel.ts +165 -7
- package/packages/core/src/editor/Topbar.ts +306 -132
- package/packages/core/src/editor/VariablePickerPanel.ts +145 -0
- package/packages/core/src/editor/styles.css +458 -59
- package/packages/core/src/index.ts +43 -2
- package/packages/core/src/renderer/index.ts +9 -1
- package/packages/core/src/renderer/mjml.ts +4 -5
- package/packages/core/src/store/store.ts +13 -2
- package/packages/core/src/types.ts +67 -2
- package/packages/core/src/utils/docClipboard.ts +89 -0
- package/packages/core/src/utils/dynamicVariantHtml.ts +45 -0
- package/packages/core/src/utils/dynamicVariantKey.ts +24 -0
- package/packages/core/src/utils/dynamicVariantSection.ts +98 -0
- package/packages/core/src/utils/emailListStyles.ts +188 -0
- package/packages/core/src/utils/inlineListEditing.ts +774 -0
- package/packages/core/src/utils/lockedMjml.ts +146 -12
- package/packages/core/src/utils/modalSize.ts +27 -0
- package/packages/core/src/utils/richHtmlEmpty.ts +43 -0
- package/packages/core/src/utils/richHtmlInsert.ts +16 -0
- package/packages/core/src/utils/sectionLayout.ts +2 -2
- package/packages/core/src/variables/index.ts +90 -0
- package/playground/vanilla/src/main.ts +26 -1
package/README.md
CHANGED
|
@@ -1,55 +1,204 @@
|
|
|
1
1
|
# Simple Mail Editor
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
解决 GrapesJS 在邮件场景下"任意嵌套 → 拖拽混乱 → 文本组件消失"的痛点。
|
|
3
|
+
面向运营的轻量邮件可视化编辑器(产品名 **Simple Mail**)。聚焦邮件场景,用**严格扁平化的组件树** + **MJML 输出引擎**,解决 GrapesJS 在邮件场景下「任意嵌套 → 拖拽混乱 → 文本组件消失」的痛点。
|
|
5
4
|
|
|
6
5
|
```text
|
|
7
|
-
Doc → Section → Column → Block
|
|
6
|
+
Doc → Section → Column → Block // 仅四层,Block 不可再嵌套子组件
|
|
8
7
|
左栏组件区 │ 中间画布区 │ 右栏属性配置区
|
|
9
8
|
```
|
|
10
9
|
|
|
10
|
+
## 目录
|
|
11
|
+
|
|
12
|
+
- [命名与 npm 包](#命名与-npm-包)
|
|
13
|
+
- [特性](#特性)
|
|
14
|
+
- [仓库结构](#仓库结构)
|
|
15
|
+
- [开发与构建](#开发与构建)
|
|
16
|
+
- [安装与集成](#安装与集成)
|
|
17
|
+
- [快速开始](#快速开始)
|
|
18
|
+
- [MailEditor API 速览](#maileditor-api-速览)
|
|
19
|
+
- [画布清空与重置](#画布清空与重置)
|
|
20
|
+
- [界面主题与品牌色](#界面主题与品牌色)
|
|
21
|
+
- [UI 选项 `ui`](#ui-选项-ui)
|
|
22
|
+
- [构造选项(画布行为)](#构造选项画布行为)
|
|
23
|
+
- [变量系统](#变量系统)
|
|
24
|
+
- [动态变量节(dynamicVariant)](#动态变量节dynamicvariant)
|
|
25
|
+
- [设计稿剪贴板](#设计稿剪贴板)
|
|
26
|
+
- [图片资源 `imageAssets`](#图片资源-imageassets)
|
|
27
|
+
- [框架集成示例](#框架集成示例)
|
|
28
|
+
- [自定义组件](#自定义组件)
|
|
29
|
+
- [宿主集成注意](#宿主集成注意)
|
|
30
|
+
- [操作手册](#操作手册)
|
|
31
|
+
- [设计与代码模式](#设计与代码模式)
|
|
32
|
+
- [数据模型速览](#数据模型速览)
|
|
33
|
+
- [路线图](#路线图)
|
|
34
|
+
- [设计取舍](#设计取舍)
|
|
35
|
+
- [许可证](#许可证)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 命名与 npm 包
|
|
40
|
+
|
|
41
|
+
三层名称各司其职,**不必强行统一成一个字符串**:
|
|
42
|
+
|
|
43
|
+
| 层级 | 名称 | 说明 |
|
|
44
|
+
|------|------|------|
|
|
45
|
+
| 仓库目录 | `simple-mail` | Git 路径、内部日志前缀(`[simple-mail]`)、剪贴板 kind(`simple-mail/doc`)、引擎标识等 |
|
|
46
|
+
| 对外子包 | `@simple-mail/core`、`@simple-mail/blocks` | **业务代码应使用的 import 名**;发布到 npm 的主包 |
|
|
47
|
+
| Monorepo 根 | `mail-editor-pancake` | 无 scope 的 `simple-mail` 在 npm 已被占用时的根包名;适合 `pnpm link` 整仓,**不作为对外 API 文档的安装名** |
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
simple-mail/ # 仓库根(package.json name: mail-editor-pancake)
|
|
51
|
+
├── packages/
|
|
52
|
+
│ ├── core/ → @simple-mail/core
|
|
53
|
+
│ └── blocks/ → @simple-mail/blocks
|
|
54
|
+
└── playground/
|
|
55
|
+
└── vanilla/ → @simple-mail/playground-vanilla(本地演示,不发布)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**推荐写法(npm 或 link 后统一用 scope):**
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { MailEditor } from '@simple-mail/core';
|
|
62
|
+
import '@simple-mail/core/style.css';
|
|
63
|
+
import { allBlocks } from '@simple-mail/blocks';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
不推荐长期依赖 `mail-editor-pancake/packages/core` 深路径;若宿主已 link 根包,请在 Vite alias 中把 `@simple-mail/*` 指到 `node_modules/mail-editor-pancake/packages/*`(见 [安装与集成](#安装与集成))。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
11
70
|
## 特性
|
|
12
71
|
|
|
13
72
|
- 框架无关核心(vanilla TS),可用于 React / Vue / 原生项目
|
|
14
|
-
- 严格两级 SortableJS 拖拽:Section 排序 + Column 内 Block
|
|
15
|
-
-
|
|
16
|
-
- **HTML
|
|
17
|
-
-
|
|
18
|
-
- 双模式:设计态 + 源码态(文档级只读 MJML/HTML
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
73
|
+
- 严格两级 SortableJS 拖拽:Section 排序 + Column 内 Block 排序;**仅从 hover 出现的 ⋮⋮ 图标拖动**,避免点选文本时误拖
|
|
74
|
+
- **画布内联编辑**:双击 Text/Button 等;浮动工具条支持加粗/斜体/下划线/删除线、字体、字号、颜色、对齐、列表、链接、清除格式
|
|
75
|
+
- **HTML 白名单清洗**:富文本进入 MJML 前清洗标签与属性;`<font>` → `<span style>`
|
|
76
|
+
- **MJML 管线**:内部 `EmailDoc` JSON → MJML → 邮件客户端可用 HTML
|
|
77
|
+
- 双模式:设计态 + 源码态(文档级只读 MJML/HTML;组件级可锁定 `lockedMjml`)
|
|
78
|
+
- 内置块:文本、HTML(raw)、图片、按钮、分隔线、间距、Hero、社交组,以及 1/2/3 列布局;示例自定义:Logo、单链社交、页脚
|
|
79
|
+
- **图片字段**:手输 URL;可选上传(`uploadImage`)、内置图库(`imageGallery`)、自管图床(`pickImageFromGallery`)
|
|
80
|
+
- 撤销/重做、键盘删除、复制 Section/Block;**复制/导入设计稿**(JSON 信封)
|
|
81
|
+
- **界面主题**:浅色 / 深色 / 跟随系统;画布仍为白纸贴近成品
|
|
82
|
+
- **品牌色**:`accentColor` / `setAccentColor`;可选顶栏拾色器
|
|
83
|
+
- **仅搭正文**:`ui.hideMailMeta` 隐藏主题、Preheader 与顶栏「邮件设置」
|
|
84
|
+
- **清空 / 重置**:`presetDoc` 与 `initialDoc` 分离
|
|
85
|
+
- **变量系统**:`setVariables`;`kind: link | image`;顶栏 `{{ }}` 弹层
|
|
86
|
+
- **动态变量节**(可选):`Section.attrs.dynamicVariantKey`,导出时整节替换为 `{{key}}`,由宿主填充(券包等)
|
|
87
|
+
- **点空白取消选中**:`clearSelectionOnCanvasMargin`
|
|
88
|
+
- 包体:核心 + MJML + CodeMirror + 富文本,gzip ≈ 580–590KB(主要来自 `mjml-browser`)
|
|
89
|
+
|
|
90
|
+
---
|
|
28
91
|
|
|
29
92
|
## 仓库结构
|
|
30
93
|
|
|
31
94
|
```text
|
|
32
95
|
simple-mail/
|
|
33
96
|
├─ packages/
|
|
34
|
-
│ ├─ core/ @simple-mail/core
|
|
35
|
-
│ └─ blocks/ @simple-mail/blocks
|
|
36
|
-
|
|
37
|
-
|
|
97
|
+
│ ├─ core/ @simple-mail/core store / 渲染 / 三栏 UI / 拖拽 / 代码模式
|
|
98
|
+
│ └─ blocks/ @simple-mail/blocks 内置块 + 示例自定义块
|
|
99
|
+
├─ playground/
|
|
100
|
+
│ └─ vanilla/ 原生 TS 演示(pnpm dev)
|
|
101
|
+
├─ package.json name: mail-editor-pancake(monorepo 根)
|
|
102
|
+
└─ pnpm-workspace.yaml
|
|
38
103
|
```
|
|
39
104
|
|
|
40
|
-
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 开发与构建
|
|
108
|
+
|
|
109
|
+
环境:**Node ≥ 20**,**pnpm ≥ 10**(见根 `packageManager`)。
|
|
41
110
|
|
|
42
111
|
```bash
|
|
112
|
+
cd simple-mail
|
|
43
113
|
pnpm install
|
|
44
|
-
pnpm dev
|
|
45
|
-
pnpm build
|
|
46
|
-
pnpm typecheck
|
|
47
|
-
pnpm
|
|
114
|
+
pnpm dev # playground,默认 http://localhost:5173
|
|
115
|
+
pnpm build # 构建 packages/core、packages/blocks → dist/
|
|
116
|
+
pnpm typecheck # 全包类型检查
|
|
117
|
+
pnpm lint # biome check
|
|
118
|
+
pnpm --filter @simple-mail/playground-vanilla build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
修改 `packages/core` 源码后,在 monorepo 根执行 **`pnpm build`**(或 `pnpm --filter @simple-mail/core build`),再让宿主重新加载,避免 link 场景下 `dist` 与源码不一致。
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 安装与集成
|
|
126
|
+
|
|
127
|
+
### 从 npm 安装(推荐)
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pnpm add @simple-mail/core @simple-mail/blocks
|
|
131
|
+
# 与 core 同版本传递依赖,宿主需能解析到:
|
|
132
|
+
pnpm add codemirror mjml-browser sortablejs
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { MailEditor } from '@simple-mail/core';
|
|
137
|
+
import '@simple-mail/core/style.css';
|
|
138
|
+
import { builtinBlocks, allBlocks } from '@simple-mail/blocks';
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- 仅需内置块时用 `builtinBlocks`;需要 Logo/页脚等示例时用 `allBlocks`,或与业务 `defineBlock` 合并。
|
|
142
|
+
- `@simple-mail/blocks` 依赖 `@simple-mail/core`,版本宜对齐。
|
|
143
|
+
|
|
144
|
+
### 本地 link 整仓(monorepo 开发)
|
|
145
|
+
|
|
146
|
+
宿主 `package.json`:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"dependencies": {
|
|
151
|
+
"mail-editor-pancake": "link:../simple-mail",
|
|
152
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
153
|
+
"@codemirror/state": "^6.5.2",
|
|
154
|
+
"@codemirror/view": "^6.36.2",
|
|
155
|
+
"codemirror": "^6.0.1",
|
|
156
|
+
"mjml-browser": "^4.15.3",
|
|
157
|
+
"sortablejs": "^1.15.6"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
48
160
|
```
|
|
49
161
|
|
|
50
|
-
|
|
162
|
+
**业务 import 仍使用 `@simple-mail/*`**,在 Vite 中 alias 到 link 后的子包路径,并排除预构建缓存:
|
|
51
163
|
|
|
52
|
-
|
|
164
|
+
```ts
|
|
165
|
+
// vite.config.ts 示意
|
|
166
|
+
const mailEditorPkgRoot = path.resolve(__dirname, 'node_modules/mail-editor-pancake/packages');
|
|
167
|
+
|
|
168
|
+
export default defineConfig({
|
|
169
|
+
resolve: {
|
|
170
|
+
alias: {
|
|
171
|
+
'@simple-mail/core': path.join(mailEditorPkgRoot, 'core'),
|
|
172
|
+
'@simple-mail/core/style.css': path.join(mailEditorPkgRoot, 'core/dist/style.css'),
|
|
173
|
+
'@simple-mail/blocks': path.join(mailEditorPkgRoot, 'blocks'),
|
|
174
|
+
// link 时 dist 内嵌套解析 codemirror / mjml-browser / sortablejs,常需指向宿主 node_modules
|
|
175
|
+
codemirror: path.dirname(require.resolve('codemirror/package.json')),
|
|
176
|
+
'mjml-browser': path.dirname(require.resolve('mjml-browser/package.json')),
|
|
177
|
+
sortablejs: path.dirname(require.resolve('sortablejs/package.json')),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
optimizeDeps: {
|
|
181
|
+
exclude: ['@simple-mail/core', '@simple-mail/blocks'],
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
仍出现「拖拽/组合拖入与源码不符」时:先 `pnpm build`(编辑器仓),再删除宿主 `node_modules/.vite` 并重启 dev。
|
|
187
|
+
|
|
188
|
+
### 发布
|
|
189
|
+
|
|
190
|
+
当前可发布子包:
|
|
191
|
+
|
|
192
|
+
| 包名 | 说明 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `@simple-mail/core` | 引擎 + 样式 `style.css` |
|
|
195
|
+
| `@simple-mail/blocks` | 内置与示例块定义 |
|
|
196
|
+
|
|
197
|
+
根包 `mail-editor-pancake` 用于占位与 link,**文档与对外示例以 `@simple-mail/*` 为准**。发布前请在各子包目录执行 build,并保证 `files` 含 `dist`。
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 快速开始
|
|
53
202
|
|
|
54
203
|
```ts
|
|
55
204
|
import { MailEditor } from '@simple-mail/core';
|
|
@@ -59,159 +208,268 @@ import { allBlocks } from '@simple-mail/blocks';
|
|
|
59
208
|
const editor = new MailEditor({
|
|
60
209
|
container: document.getElementById('app')!,
|
|
61
210
|
blocks: allBlocks,
|
|
62
|
-
/**
|
|
63
|
-
* initialDoc 为 Partial<EmailDoc>:可预置 meta / styles / variables / sections(画布结构)。
|
|
64
|
-
* 未写的字段会与默认空邮件合并;仅搭正文且由宿主管发件主题时可将 subject、preheader 留空,并配合 ui.hideMailMeta。
|
|
65
|
-
*/
|
|
66
211
|
initialDoc: {
|
|
67
212
|
meta: { subject: '欢迎', width: 600 },
|
|
68
213
|
variables: [{ key: 'user.name', label: '用户名', sample: '张三' }],
|
|
69
214
|
sections: [],
|
|
70
215
|
},
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* 编辑页打开已保存邮件时:initialDoc = 当前稿,presetDoc = 业务默认模板。
|
|
74
|
-
* 未传时与 initialDoc 合并结果一致。
|
|
75
|
-
*/
|
|
76
|
-
// presetDoc: defaultTemplatePartial,
|
|
77
|
-
/** 右栏控件形态等,见下文「UI 选项 ui」 */
|
|
78
|
-
// ui: { preferSliderControls: true, hideMailMeta: true },
|
|
79
|
-
// autoWrapSection: true(默认)— 把 Block 拖到 Section 之间空白处时
|
|
80
|
-
// 自动包一个一列 Section。设为 false 则强制只能拖入现有列内。
|
|
81
|
-
// 唯一块被删或拖走后,会去掉因此变空的 Section,无需再删一次壳子。
|
|
216
|
+
// presetDoc: businessDefault, // 顶栏「重置内容」目标,见下文
|
|
217
|
+
// ui: { hideMailMeta: true, preferSliderControls: true },
|
|
82
218
|
autoWrapSection: true,
|
|
83
219
|
onChange: (doc) => console.log(doc),
|
|
84
|
-
/**
|
|
85
|
-
* 可选:见 README「图片资源 imageAssets」(uploadImage、内置 imageGallery、自管 pickImageFromGallery)。
|
|
86
|
-
*/
|
|
87
|
-
// imageAssets 见下方;演示见 playground(内置图库 + 侧栏上传)
|
|
88
|
-
// imageAssets: { uploadImage, imageGallery: adapter, showGallery: true },
|
|
89
220
|
});
|
|
90
221
|
|
|
91
222
|
const { mjml, html } = editor.export({ withSampleVariables: true });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
构造后建议注入业务变量(避免只写在 `initialDoc.variables` 里后被空数组覆盖):
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
editor.setVariables([
|
|
229
|
+
{ key: 'username', label: '用户名', sample: '张三' },
|
|
230
|
+
{ key: 'couponLink', label: '优惠券链接', kind: 'link', sample: '#' },
|
|
231
|
+
]);
|
|
232
|
+
```
|
|
92
233
|
|
|
93
|
-
|
|
94
|
-
// editor.setValue(nextDoc);
|
|
234
|
+
---
|
|
95
235
|
|
|
96
|
-
|
|
97
|
-
// editor.clearCanvas();
|
|
236
|
+
## MailEditor API 速览
|
|
98
237
|
|
|
99
|
-
|
|
100
|
-
|
|
238
|
+
| 方法 / 属性 | 说明 |
|
|
239
|
+
|-------------|------|
|
|
240
|
+
| `store` | 内部 Store,高级场景可读状态 |
|
|
241
|
+
| `registry` | 块注册表 |
|
|
242
|
+
| `getValue()` / `setValue(doc)` | 读写完整 `EmailDoc`;`setValue` 清空撤销栈 |
|
|
243
|
+
| `export({ withSampleVariables? })` | `{ mjml, html }` |
|
|
244
|
+
| `setVariables` / `getVariables` | 变量列表;`setValue` 后仍会写回 |
|
|
245
|
+
| `insertVariableKey` / `insertVariableElement` / `insertVariable` | 插入占位符 |
|
|
246
|
+
| `clearCanvas()` / `resetToPreset()` / `setPresetDoc(partial)` | 画布清空与重置 |
|
|
247
|
+
| `setTheme` / `getTheme` | `light` \| `dark` \| `system` |
|
|
248
|
+
| `setAccentColor` / `getAccentColor` | 品牌色 `#RRGGBB` |
|
|
249
|
+
| `copyDocDesign()` | 复制设计稿 JSON 到剪贴板 |
|
|
250
|
+
| `openImportDocDesign()` / `importDocDesignFromJson(raw)` | 导入设计稿 |
|
|
251
|
+
| `registerBlock(def)` | 运行时注册块 |
|
|
252
|
+
| `setSelection(sel \| null)` | 程序化选中 |
|
|
253
|
+
| `destroy()` | 卸载 DOM 与监听 |
|
|
101
254
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
255
|
+
更多工具函数见 `@simple-mail/core` 导出(变量 HTML、动态变量节、`docClipboard`、`openImageGalleryModal` 等)。
|
|
256
|
+
|
|
257
|
+
---
|
|
105
258
|
|
|
106
|
-
|
|
259
|
+
## 画布清空与重置
|
|
107
260
|
|
|
108
|
-
|
|
261
|
+
顶栏位于撤销/重做右侧(可用 `ui.hideTopbarClearCanvas` / `hideTopbarResetContent` 隐藏):
|
|
109
262
|
|
|
110
263
|
| 按钮 | 行为 |
|
|
111
264
|
|------|------|
|
|
112
|
-
| **清空画布** | 移除所有 Section
|
|
113
|
-
| **重置内容** |
|
|
114
|
-
|
|
115
|
-
**宿主常见写法**:新建页 `initialDoc` 与 `presetDoc` 同为默认模板;编辑页 `initialDoc` 为接口返回的 `jsonContent`,`presetDoc` 仍为业务预置结构,避免「重置」把用户带回打开时的草稿。
|
|
265
|
+
| **清空画布** | 移除所有 Section/Block;保留 `meta`、`styles`、`variables`;可撤销 |
|
|
266
|
+
| **重置内容** | 恢复为 **`presetDoc`** 快照;未传时等同构造时 `initialDoc` 合并结果 |
|
|
116
267
|
|
|
117
268
|
```ts
|
|
118
269
|
const editor = new MailEditor({
|
|
119
270
|
container: el,
|
|
120
271
|
blocks: allBlocks,
|
|
121
|
-
initialDoc: loadedFromApi,
|
|
122
|
-
presetDoc: businessDefaultTemplate,
|
|
272
|
+
initialDoc: loadedFromApi,
|
|
273
|
+
presetDoc: businessDefaultTemplate,
|
|
123
274
|
onChange: (doc) => save(doc),
|
|
124
275
|
});
|
|
125
276
|
```
|
|
126
277
|
|
|
127
|
-
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 界面主题与品牌色
|
|
128
281
|
|
|
129
|
-
构造参数 `theme?: 'light' | 'dark' | 'system'`(默认 `light
|
|
282
|
+
构造参数 `theme?: 'light' | 'dark' | 'system'`(默认 `light`)。
|
|
130
283
|
|
|
131
284
|
```ts
|
|
132
285
|
editor.setTheme('dark');
|
|
133
|
-
editor.setTheme('system'); // 随系统明暗
|
|
134
286
|
editor.getTheme();
|
|
135
287
|
```
|
|
136
288
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
### 品牌色(强调色)
|
|
140
|
-
|
|
141
|
-
界面主色对应 CSS 变量 `--sm-primary`、`--sm-primary-soft`(主按钮、选区、链接强调等)。不传则随 light/dark/system 使用内置紫/靛。
|
|
289
|
+
根节点 `data-sm-theme`;可在宿主侧覆盖 `.sm-root` CSS 变量。
|
|
142
290
|
|
|
143
291
|
| 方式 | 说明 |
|
|
144
292
|
|------|------|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
293
|
+
| `accentColor?: string` | 构造时 `#RRGGBB`;无效值告警并忽略 |
|
|
294
|
+
| `showAccentColorPicker?: boolean` | 顶栏原生颜色控件,默认 `false` |
|
|
295
|
+
| `setAccentColor(hex \| null \| '')` | 运行时覆盖;空则恢复默认 |
|
|
296
|
+
| `getAccentColor()` | 仅显式覆盖;未设返回 `undefined` |
|
|
149
297
|
|
|
150
|
-
|
|
298
|
+
---
|
|
151
299
|
|
|
152
|
-
|
|
300
|
+
## UI 选项 `ui`
|
|
153
301
|
|
|
154
302
|
`MailEditor` 的 `ui?: EditorUiOptions`:
|
|
155
303
|
|
|
156
304
|
| 字段 | 说明 |
|
|
157
305
|
|------|------|
|
|
158
|
-
| `preferSliderControls
|
|
159
|
-
| `hideMailMeta
|
|
160
|
-
| `hideTopbarTitle
|
|
161
|
-
| `hideTopbarMailSettings
|
|
162
|
-
| `hideTopbarFullscreen
|
|
163
|
-
| `hideTopbarClearCanvas
|
|
164
|
-
| `
|
|
306
|
+
| `preferSliderControls?` | 右栏数值/内边距/宽度等用滑块增强控件;字重仍为五档平铺 |
|
|
307
|
+
| `hideMailMeta?` | 隐藏右栏主题、Preheader;文档级仅「版式 + 全局样式」 |
|
|
308
|
+
| `hideTopbarTitle?` | 隐藏顶栏产品标题 |
|
|
309
|
+
| `hideTopbarMailSettings?` | 隐藏顶栏「邮件设置」 |
|
|
310
|
+
| `hideTopbarFullscreen?` | 隐藏全屏按钮 |
|
|
311
|
+
| `hideTopbarClearCanvas?` / `hideTopbarResetContent?` | 隐藏清空 / 重置 |
|
|
312
|
+
| `hideTopbarDocClipboard?` | 隐藏「复制设计稿」「导入设计稿」 |
|
|
313
|
+
| `hideTopbarInsertVariable?` | 隐藏顶栏「插入变量」(由宿主提供时) |
|
|
314
|
+
| `topbarCompact?` | 顶栏默认仅图标(窄屏/嵌入) |
|
|
315
|
+
| `topbarLabels?` | `auto`(默认)\| `never` \| `always`;与 `topbarCompact` 配合 |
|
|
316
|
+
| `topbarCompactMinWidth?` | `topbarLabels: auto` 时展示文案的最小宽度 px,默认 1200 |
|
|
317
|
+
| `paletteBlockGroupTitle?` | 左栏合并分组标题,默认「组件」 |
|
|
318
|
+
| `customPaletteTooltipSuffix?` | custom 块无 `paletteTooltip` 时的 title 后缀 |
|
|
319
|
+
| `hiddenPaletteBlockTypes?` | 注册但不显示在左栏的 type(如组合块内部用的 divider) |
|
|
320
|
+
| `enableDynamicVariantKey?` | 开启 Section「动态变量名」、画布标识、palette 动态节;默认 `false` |
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 构造选项(画布行为)
|
|
165
325
|
|
|
166
|
-
|
|
326
|
+
| 字段 | 说明 |
|
|
327
|
+
|------|------|
|
|
328
|
+
| `container` | 挂载 DOM(必填) |
|
|
329
|
+
| `blocks?` | 块定义列表 |
|
|
330
|
+
| `engine?` | 目前仅 `'mjml'`;预留 `'table'` |
|
|
331
|
+
| `initialDoc?` | 首次进入画布 |
|
|
332
|
+
| `presetDoc?` | 「重置内容」目标 |
|
|
333
|
+
| `autoWrapSection?` | 默认 `true`:拖到 Section 间隙自动包单列 Section |
|
|
334
|
+
| `clearSelectionOnCanvasMargin?` | 点画布留白取消选中 |
|
|
335
|
+
| `theme?` / `accentColor?` / `showAccentColorPicker?` | 见上 |
|
|
336
|
+
| `imageAssets?` | 见 [图片资源](#图片资源-imageassets) |
|
|
337
|
+
| `onChange?` | 文档变更(防抖) |
|
|
167
338
|
|
|
168
|
-
|
|
339
|
+
---
|
|
169
340
|
|
|
170
|
-
|
|
341
|
+
## 变量系统
|
|
171
342
|
|
|
172
|
-
|
|
343
|
+
### 数据模型
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
interface Variable {
|
|
347
|
+
key: string;
|
|
348
|
+
label: string;
|
|
349
|
+
sample?: string;
|
|
350
|
+
kind?: 'text' | 'link' | 'image';
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| `kind` | 「插入元素」行为 |
|
|
355
|
+
|--------|------------------|
|
|
356
|
+
| `text` | `{{key}}` |
|
|
357
|
+
| `link` | `<a href="{{key}}">…</a>` |
|
|
358
|
+
| `image` | 新建 image 块,`src` 为 `{{key}}` |
|
|
359
|
+
|
|
360
|
+
`setValue` / `resetToPreset` 后,通过 `setVariables` 注入的列表仍会写回 `doc.variables`。
|
|
361
|
+
|
|
362
|
+
### 插入 API
|
|
363
|
+
|
|
364
|
+
| 方法 | 说明 |
|
|
173
365
|
|------|------|
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `showUpload?` | 是否显示「上传」;仅当配置了 `uploadImage` 时有效,**默认 `true`**。 |
|
|
178
|
-
| `showGallery?` | 是否显示「图床」;配置了 `imageGallery` **或** `pickImageFromGallery` 时有效,**默认 `false`**。 |
|
|
366
|
+
| `insertVariableKey(v)` | 纯文本 `{{key}}` |
|
|
367
|
+
| `insertVariableElement(v)` | link / image 片段 |
|
|
368
|
+
| `insertVariable(v)` | 同 `insertVariableKey`(兼容) |
|
|
179
369
|
|
|
180
|
-
|
|
370
|
+
优先级:内联编辑光标 → 聚焦的 input/textarea → 选中 Block 主文本字段 → 末尾新建 text 块。
|
|
181
371
|
|
|
182
|
-
|
|
372
|
+
### 宿主工具函数
|
|
183
373
|
|
|
184
|
-
|
|
374
|
+
```ts
|
|
375
|
+
import {
|
|
376
|
+
buildBodyVariableKeyInsert,
|
|
377
|
+
buildBodyVariableElementInsert,
|
|
378
|
+
buildLinkVariableHtml,
|
|
379
|
+
normalizeVariable,
|
|
380
|
+
tokenToVariableKey,
|
|
381
|
+
variablePlaceholder,
|
|
382
|
+
} from '@simple-mail/core';
|
|
383
|
+
```
|
|
185
384
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
385
|
+
`buildBodyVariableInsert` 仍可用,新代码请用 `buildBodyVariableKeyInsert` / `buildBodyVariableElementInsert`。
|
|
386
|
+
|
|
387
|
+
### 入口分工(建议)
|
|
388
|
+
|
|
389
|
+
| 场景 | 入口 |
|
|
390
|
+
|------|------|
|
|
391
|
+
| 发件主题、Preheader 等 | **宿主页**插入变量 |
|
|
392
|
+
| 正文 | **编辑器顶栏** `{{ }}`(`hideTopbarInsertVariable` 时可关掉) |
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 动态变量节(dynamicVariant)
|
|
397
|
+
|
|
398
|
+
用于「整节内容由宿主按 key 替换」的场景(如券包 `couponGroup`)。Section 设置 `attrs.dynamicVariantKey` 后,导出 HTML/MJML 时该节正文替换为 `{{key}}`;具体 HTML 由宿主写入。
|
|
399
|
+
|
|
400
|
+
- 编辑器 UI 默认关闭;宿主设 `ui.enableDynamicVariantKey: true` 后,右栏可编辑 key,画布显示标识。
|
|
401
|
+
- 左栏 `expandPaletteDrop` 可返回 `{ blocks, sectionAttrs: { dynamicVariantKey: '…' } }` 一次拖入动态节。
|
|
402
|
+
- 已写入文档的 key **不依赖** UI 开关,导出仍生效。
|
|
403
|
+
|
|
404
|
+
宿主可从 core 导入:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import {
|
|
408
|
+
DYNAMIC_VARIANT_HTML_ATTR,
|
|
409
|
+
dynamicVariantPlaceholder,
|
|
410
|
+
extractDynamicVariantSlots,
|
|
411
|
+
getSectionDynamicVariantKey,
|
|
412
|
+
isDynamicVariantSection,
|
|
413
|
+
renderSectionBodyHtml,
|
|
414
|
+
annotateDynamicVariantHtmlAttributes,
|
|
415
|
+
} from '@simple-mail/core';
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`Section.attrs.meta` 为宿主扩展袋,编辑器不解释,随 `EmailDoc` 序列化。
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## 设计稿剪贴板
|
|
423
|
+
|
|
424
|
+
顶栏 **复制设计稿** / **导入设计稿**(`ui.hideTopbarDocClipboard` 可隐藏)。
|
|
425
|
+
|
|
426
|
+
- 复制:信封 `{ kind: 'simple-mail/doc', formatVersion: 1, doc }` 写入系统剪贴板。
|
|
427
|
+
- 导入:粘贴 JSON 或裸 `EmailDoc`(`version: '1'`),覆盖当前画布(可撤销)。
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
await editor.copyDocDesign();
|
|
431
|
+
editor.openImportDocDesign();
|
|
432
|
+
editor.importDocDesignFromJson(raw); // 与对话框「应用」相同
|
|
433
|
+
|
|
434
|
+
import {
|
|
435
|
+
DOC_CLIPBOARD_KIND,
|
|
436
|
+
parseDocClipboard,
|
|
437
|
+
regenerateDocIds,
|
|
438
|
+
serializeDocClipboard,
|
|
439
|
+
} from '@simple-mail/core';
|
|
191
440
|
```
|
|
192
441
|
|
|
193
|
-
|
|
194
|
-
|------|:----:|------|
|
|
195
|
-
| `listItems({ query, page })` | ✅ | `query` 为搜索框文本;`page` 从 **0** 起。返回 `{ items: GalleryItem[], hasMore }`。 |
|
|
196
|
-
| `uploadFile?(file)` | | 若提供,工具栏显示「**上传**」(与搜索、添加同一行);完成后重新请求第 0 页。 |
|
|
197
|
-
| `addByUrl?(url)` | | 若提供,工具栏显示链接输入与「**添加**」;完成后重新请求第 0 页;校验/落库由宿主完成,失败请 `throw`。 |
|
|
198
|
-
| `deleteItem?(id)` | | 若提供,每张缩略图右上角可删除;成功后重新请求第 0 页;失败请 `throw`。 |
|
|
442
|
+
跨实例迁移时可用 `regenerateDocIds` 避免 id 冲突。
|
|
199
443
|
|
|
200
|
-
|
|
444
|
+
---
|
|
201
445
|
|
|
202
|
-
|
|
446
|
+
## 图片资源 `imageAssets`
|
|
203
447
|
|
|
204
|
-
|
|
448
|
+
编辑器只存 URL;落地由宿主实现。
|
|
205
449
|
|
|
206
|
-
|
|
450
|
+
| 字段 | 作用 |
|
|
451
|
+
|------|------|
|
|
452
|
+
| `uploadImage?(file, ctx)` | 右栏「上传」→ HTTPS URL |
|
|
453
|
+
| `imageGallery?` | 内置图库弹层(`showGallery: true`) |
|
|
454
|
+
| `pickImageFromGallery?(ctx)` | 自管图床;与 `imageGallery` 并存时**优先内置图库** |
|
|
455
|
+
| `showUpload?` | 默认 `true`(有 `uploadImage` 时) |
|
|
456
|
+
| `showGallery?` | 默认 `false` |
|
|
207
457
|
|
|
208
|
-
|
|
209
|
-
需要本库弹层时:配 `imageGallery` + `showGallery: true`。
|
|
210
|
-
若两者都配,点击「图床」**走 `imageGallery`**。
|
|
458
|
+
`ImageFieldContext`:`blockId` / `propKey` / `currentUrl`。
|
|
211
459
|
|
|
212
|
-
|
|
460
|
+
内置图库实现 `ImageGalleryAdapter`:`listItems({ query, page })`(`page` 从 0 起)、可选 `uploadFile` / `addByUrl` / `deleteItem`。弹层根 `.sm-gallery-modal`,可用 CSS 变量改主题。
|
|
213
461
|
|
|
214
|
-
|
|
462
|
+
```ts
|
|
463
|
+
import { openImageGalleryModal } from '@simple-mail/core';
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
邮件图片需公网 URL;`data:` / CID 需自行扩展 MJML。
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 框架集成示例
|
|
471
|
+
|
|
472
|
+
### React
|
|
215
473
|
|
|
216
474
|
```tsx
|
|
217
475
|
import { useEffect, useRef } from 'react';
|
|
@@ -219,8 +477,13 @@ import { MailEditor, type EmailDoc } from '@simple-mail/core';
|
|
|
219
477
|
import '@simple-mail/core/style.css';
|
|
220
478
|
import { allBlocks } from '@simple-mail/blocks';
|
|
221
479
|
|
|
222
|
-
export function MailEditorView({
|
|
223
|
-
value
|
|
480
|
+
export function MailEditorView({
|
|
481
|
+
value,
|
|
482
|
+
presetDoc,
|
|
483
|
+
onChange,
|
|
484
|
+
}: {
|
|
485
|
+
value?: Partial<EmailDoc>;
|
|
486
|
+
presetDoc?: Partial<EmailDoc>;
|
|
224
487
|
onChange?: (d: EmailDoc) => void;
|
|
225
488
|
}) {
|
|
226
489
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -229,6 +492,8 @@ export function MailEditorView({ value, onChange }: {
|
|
|
229
492
|
container: ref.current!,
|
|
230
493
|
blocks: allBlocks,
|
|
231
494
|
initialDoc: value,
|
|
495
|
+
presetDoc,
|
|
496
|
+
ui: { hideTopbarTitle: true },
|
|
232
497
|
onChange,
|
|
233
498
|
});
|
|
234
499
|
return () => editor.destroy();
|
|
@@ -237,7 +502,7 @@ export function MailEditorView({ value, onChange }: {
|
|
|
237
502
|
}
|
|
238
503
|
```
|
|
239
504
|
|
|
240
|
-
### Vue
|
|
505
|
+
### Vue 3
|
|
241
506
|
|
|
242
507
|
```vue
|
|
243
508
|
<script setup lang="ts">
|
|
@@ -246,19 +511,25 @@ import { MailEditor, type BlockDefinition, type EmailDoc } from '@simple-mail/co
|
|
|
246
511
|
import '@simple-mail/core/style.css';
|
|
247
512
|
import { allBlocks } from '@simple-mail/blocks';
|
|
248
513
|
|
|
249
|
-
const props = defineProps<{
|
|
514
|
+
const props = defineProps<{
|
|
515
|
+
modelValue?: Partial<EmailDoc>;
|
|
516
|
+
presetDoc?: Partial<EmailDoc>;
|
|
517
|
+
blocks?: BlockDefinition<any>[];
|
|
518
|
+
}>();
|
|
250
519
|
const emit = defineEmits<{ 'update:modelValue': [EmailDoc] }>();
|
|
251
520
|
const el = ref<HTMLDivElement>();
|
|
252
521
|
let editor: MailEditor | null = null;
|
|
253
522
|
|
|
254
523
|
onMounted(() => {
|
|
255
524
|
const defs = props.blocks ?? allBlocks;
|
|
256
|
-
// expandPaletteDrop / schema 等依赖块定义上的函数;props 深度代理可能导致丢失,建议 markRaw
|
|
257
525
|
const stable = defs.map((d) => markRaw(d));
|
|
258
526
|
editor = new MailEditor({
|
|
259
527
|
container: el.value!,
|
|
260
528
|
blocks: stable,
|
|
261
529
|
initialDoc: props.modelValue,
|
|
530
|
+
presetDoc: props.presetDoc,
|
|
531
|
+
ui: { hideMailMeta: true, hideTopbarTitle: true },
|
|
532
|
+
clearSelectionOnCanvasMargin: true,
|
|
262
533
|
onChange: (doc) => emit('update:modelValue', doc),
|
|
263
534
|
});
|
|
264
535
|
});
|
|
@@ -266,160 +537,169 @@ onBeforeUnmount(() => editor?.destroy());
|
|
|
266
537
|
</script>
|
|
267
538
|
|
|
268
539
|
<template>
|
|
269
|
-
<div ref="el" style="height: 100vh" />
|
|
540
|
+
<div ref="el" class="simple-mail-editor-host" style="height: 100vh" />
|
|
270
541
|
</template>
|
|
271
542
|
```
|
|
272
543
|
|
|
273
|
-
|
|
544
|
+
`blocks` 经 Vue 响应式代理时,`expandPaletteDrop` 等函数字段可能失效,务必 **`markRaw`** 每个 `BlockDefinition`。
|
|
545
|
+
|
|
546
|
+
---
|
|
274
547
|
|
|
275
|
-
|
|
548
|
+
## 自定义组件
|
|
276
549
|
|
|
277
550
|
```ts
|
|
278
551
|
import { defineBlock } from '@simple-mail/core';
|
|
279
552
|
|
|
280
|
-
export const couponBlock = defineBlock<{ title: string; code: string
|
|
553
|
+
export const couponBlock = defineBlock<{ title: string; code: string }>({
|
|
281
554
|
type: 'custom:coupon',
|
|
282
555
|
name: '优惠券',
|
|
283
556
|
category: 'custom',
|
|
284
|
-
icon: '<svg width="24" height="24"
|
|
285
|
-
defaultProps: { title: '专属优惠', code: 'WELCOME10'
|
|
557
|
+
icon: '<svg width="24" height="24">…</svg>',
|
|
558
|
+
defaultProps: { title: '专属优惠', code: 'WELCOME10' },
|
|
286
559
|
schema: [
|
|
287
560
|
{ key: 'code', label: '券码', type: 'text' },
|
|
288
|
-
{ key: '
|
|
561
|
+
{ key: 'title', label: '标题', type: 'text', inheritGlobal: true },
|
|
289
562
|
],
|
|
290
|
-
// 让 title 字段支持画布内双击编辑(可选)
|
|
291
563
|
inlineEditable: {
|
|
292
564
|
selector: '.coupon-title',
|
|
293
|
-
mode: 'rich',
|
|
565
|
+
mode: 'rich',
|
|
294
566
|
multiline: false,
|
|
295
567
|
propKey: 'title',
|
|
296
|
-
placeholder: '
|
|
568
|
+
placeholder: '双击编辑',
|
|
297
569
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<span class="coupon-title">${p.title}</span>
|
|
301
|
-
🎁 ${p.code} <small>(到期 ${p.expiresAt})</small>
|
|
302
|
-
</mj-text>`,
|
|
303
|
-
// 设计态画布的轻量预览(可选)。inlineEditable.selector 必须能命中此预览中的元素。
|
|
304
|
-
renderPreview: (p) => `<div style="text-align:center;padding:16px;font-weight:700;font-size:20px;">
|
|
305
|
-
<span class="coupon-title">${p.title}</span>
|
|
306
|
-
🎁 ${p.code} <small>(到期 ${p.expiresAt})</small>
|
|
307
|
-
</div>`,
|
|
570
|
+
toMjml: (p) => `<mj-text>…</mj-text>`,
|
|
571
|
+
renderPreview: (p) => `<div class="coupon-title">…</div>`,
|
|
308
572
|
});
|
|
309
573
|
|
|
310
574
|
new MailEditor({ container, blocks: [...allBlocks, couponBlock] });
|
|
311
575
|
```
|
|
312
576
|
|
|
313
|
-
`schema`
|
|
314
|
-
**社交组**(`social-group`)另有 **`iconBorderRadius`**(px,默认圆形)、**`iconSpacing`**(图标间距 px,对应 MJML `mj-social` 的 `inner-padding`),画布预览与导出共用同一套圆角/间距逻辑。
|
|
315
|
-
所有字段会在右栏自动渲染表单,change 事件回写 `block.props`。
|
|
577
|
+
### `schema` 字段类型
|
|
316
578
|
|
|
317
|
-
|
|
579
|
+
`text | textarea | number | color | select | switch | image | url | spacing | socialLinkList`
|
|
318
580
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
| `mode` | `rich`(保留 HTML,配合富文本工具条)/ `plain`(纯文本,单行/多行) |
|
|
323
|
-
| `multiline` | `false` 时回车提交且禁止换行(按钮文字常用) |
|
|
324
|
-
| `propKey` | 提交时把 innerHTML(rich)或 textContent(plain)写回该 key |
|
|
325
|
-
| `placeholder` | 当字段为空时显示的占位提示 |
|
|
326
|
-
|
|
327
|
-
提交时机:失焦 / 单行 Enter / Esc 取消。富文本提交前会过一次白名单清洗(保留 `a/b/strong/i/em/u/s/span/p/div/ul/ol/li/h1-h6/br`,属性仅留 `href/target/rel/style/class`,`<font>` 自动转为 `<span style="...">`)。
|
|
581
|
+
- `selectVariant: 'segmented'`:少量互斥选项(如对齐)
|
|
582
|
+
- `inheritGlobal: true`:右栏「继承全局」开关
|
|
583
|
+
- `image`:由 `imageAssets` 控制上传/图库
|
|
328
584
|
|
|
329
|
-
###
|
|
585
|
+
### `inlineEditable`
|
|
330
586
|
|
|
331
|
-
|
|
587
|
+
| 字段 | 说明 |
|
|
588
|
+
|------|------|
|
|
589
|
+
| `selector?` | 在 `renderPreview` DOM 中定位;缺省为根 |
|
|
590
|
+
| `mode` | `rich`(清洗 HTML)\| `plain` \| `html`(原始 HTML,mj-raw) |
|
|
591
|
+
| `multiline?` | `false` 时 Enter 提交 |
|
|
592
|
+
| `propKey` | 写回的 props 键 |
|
|
593
|
+
| `placeholder?` | 空值提示 |
|
|
332
594
|
|
|
333
|
-
|
|
595
|
+
### `expandPaletteDrop`(组合拖入)
|
|
334
596
|
|
|
335
597
|
```ts
|
|
336
|
-
expandPaletteDrop?: (createBlock
|
|
598
|
+
expandPaletteDrop?: (createBlock) => Block[] | { blocks: Block[]; sectionAttrs?: Partial<SectionAttrs> };
|
|
337
599
|
```
|
|
338
600
|
|
|
339
|
-
|
|
340
|
-
- 回调内请使用传入的 `createBlock('image' | 'text' | …)`,以便 ID、`defaultProps` 与内置块一致。
|
|
341
|
-
- `toMjml` / `renderPreview` 仍须在类型上满足 `BlockDefinition`;组合入口可选用占位的 `toMjml: () => ''`(正常不应出现在 `sections` 里)。
|
|
601
|
+
从左栏拖入时展开为多个通用块;文档 JSON **不出现**该 palette 的 `type`。拖到 Section 间隙时可带 `sectionAttrs`(含 `dynamicVariantKey`、`meta`)。
|
|
342
602
|
|
|
343
|
-
|
|
603
|
+
---
|
|
344
604
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
605
|
+
## 宿主集成注意
|
|
606
|
+
|
|
607
|
+
1. 改 core 源码后 **`pnpm build`**,再刷新宿主。
|
|
608
|
+
2. **Vue**:`blocks` 用 `markRaw`。
|
|
609
|
+
3. **Vite link**:`optimizeDeps.exclude` 含 `@simple-mail/core`、`@simple-mail/blocks`;配置 alias 与 codemirror/mjml/sortablejs 解析。
|
|
610
|
+
4. **import 路径**:统一 `@simple-mail/*`,避免长期依赖 `mail-editor-pancake/packages/...`。
|
|
611
|
+
5. 宿主需安装与 core 一致的 **codemirror、mjml-browser、sortablejs**(link 时尤甚)。
|
|
612
|
+
|
|
613
|
+
---
|
|
348
614
|
|
|
349
615
|
## 操作手册
|
|
350
616
|
|
|
351
617
|
| 操作 | 触发方式 |
|
|
352
|
-
|
|
353
|
-
| 选中 Section | 单击
|
|
354
|
-
|
|
|
355
|
-
|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
359
|
-
|
|
|
360
|
-
|
|
|
361
|
-
|
|
|
362
|
-
|
|
|
363
|
-
|
|
|
364
|
-
|
|
|
365
|
-
|
|
|
366
|
-
|
|
|
367
|
-
|
|
|
618
|
+
|------|----------|
|
|
619
|
+
| 选中 Section / Block | 单击 |
|
|
620
|
+
| 编辑内容 | 双击;或选中后 ✎ |
|
|
621
|
+
| 提交 / 取消编辑 | 失焦、单行 Enter / Esc |
|
|
622
|
+
| 列表 | Shift+Enter 软换行;空项 Enter 退出列表 |
|
|
623
|
+
| 拖拽排序 | 仅 ⋮⋮ 图标 |
|
|
624
|
+
| 删除 | Delete/Backspace 或工具条 |
|
|
625
|
+
| 复制块/节 | 工具条 ⎘ |
|
|
626
|
+
| 撤销/重做 | ⌘Z / ⌘⇧Z |
|
|
627
|
+
| 清空 / 重置 | 顶栏按钮或 API |
|
|
628
|
+
| 变量 | 顶栏 `{{ }}` 或 API |
|
|
629
|
+
| 设计稿 | 顶栏复制/导入 |
|
|
630
|
+
| 点空白取消选中 | `clearSelectionOnCanvasMargin: true` |
|
|
631
|
+
| Esc(设计态) | 块 → Section → 文档级面板 |
|
|
632
|
+
| 源码/设计切换 | 顶栏 |
|
|
633
|
+
| 导出 HTML | 顶栏 |
|
|
634
|
+
|
|
635
|
+
---
|
|
368
636
|
|
|
369
637
|
## 设计与代码模式
|
|
370
638
|
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
|
|
375
|
-
- **导出 HTML**:顶栏右上角,下载经 `withSampleVariables` 替换后的 HTML 文件。
|
|
376
|
-
|
|
377
|
-
## 数据模型 速览
|
|
639
|
+
- **设计模式**:三栏 WYSIWYG 预览(非真实邮件 HTML)。
|
|
640
|
+
- **源码模式**:只读整份 MJML + 编译 HTML,可复制。
|
|
641
|
+
- **组件级代码**(右栏「代码」Tab):编辑 `lockedMjml`,禁用属性面板,可恢复默认。
|
|
642
|
+
- **导出 HTML**:顶栏下载,`withSampleVariables` 替换示例值。
|
|
378
643
|
|
|
379
|
-
|
|
644
|
+
---
|
|
380
645
|
|
|
381
|
-
|
|
382
|
-
- **`presetDoc`**:可选,与默认空邮件合并,作为顶栏 **「重置内容」** 的目标;未传时与 `initialDoc` 相同。编辑已保存邮件时应单独传入业务默认模板,勿与 `initialDoc` 混用。
|
|
383
|
-
- **`editor.setValue(doc)`**:运行期整份替换(如切换模板);会清空撤销栈;调用前会尽量失焦右栏输入。
|
|
384
|
-
- **`editor.clearCanvas()`** / **`editor.resetToPreset()`** / **`editor.setPresetDoc(partial)`**:与顶栏按钮等价,供宿主程序化调用。
|
|
646
|
+
## 数据模型速览
|
|
385
647
|
|
|
386
648
|
```ts
|
|
387
649
|
interface EmailDoc {
|
|
388
650
|
version: '1';
|
|
389
651
|
meta: { subject: string; preheader?: string; width: number | string };
|
|
390
|
-
variables:
|
|
391
|
-
styles:
|
|
392
|
-
sections: Section[];
|
|
652
|
+
variables: Variable[];
|
|
653
|
+
styles: GlobalStyles; // 含 fontWeight 档位、listIndentDefaultPx 等
|
|
654
|
+
sections: Section[];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
interface Section {
|
|
658
|
+
id: string;
|
|
659
|
+
type: 'section';
|
|
660
|
+
layout: '1' | '1-1' | '1-2' | '2-1' | '1-1-1';
|
|
661
|
+
attrs: SectionAttrs; // padding、preserveColumnsOnMobile、columnGap、width、dynamicVariantKey、meta
|
|
662
|
+
columns: Column[];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
interface Block {
|
|
666
|
+
id: string;
|
|
667
|
+
type: string;
|
|
668
|
+
props: Record<string, unknown>;
|
|
669
|
+
lockedMjml?: string;
|
|
393
670
|
}
|
|
394
|
-
interface Section { id; type: 'section'; layout: '1'|'1-1'|'1-2'|'2-1'|'1-1-1'; attrs; columns: Column[] }
|
|
395
|
-
// attrs.preserveColumnsOnMobile:多列时可设 true,MJML 包 mj-group,小屏仍为并排列;默认/未设则小屏堆叠列
|
|
396
|
-
// attrs.width:本节最大宽度(px 或 %),窄于邮件宽度时居中;不设则同邮件 meta.width
|
|
397
|
-
// attrs.columnGap:多列时列间距 (px),MJML 通过列对称 padding 实现;单列无效
|
|
398
|
-
interface Column { id; attrs; blocks: Block[] }
|
|
399
|
-
interface Block { id; type; props; lockedMjml? }
|
|
400
671
|
```
|
|
401
672
|
|
|
673
|
+
- `initialDoc`:首次画布。
|
|
674
|
+
- `presetDoc`:重置目标;编辑已保存邮件时勿与 `initialDoc` 混为一谈。
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
402
678
|
## 路线图
|
|
403
679
|
|
|
404
|
-
- [x] M1 ·
|
|
405
|
-
- [x] M2 · 三栏 UI
|
|
406
|
-
- [x] M3 ·
|
|
407
|
-
- [ ] M4 · `@simple-mail/react` / `@simple-mail/vue`
|
|
408
|
-
- [ ] iframe
|
|
409
|
-
- [ ]
|
|
680
|
+
- [x] M1 · Schema → MJML → HTML + 内置组件
|
|
681
|
+
- [x] M2 · 三栏 UI、拖拽、撤销/重做
|
|
682
|
+
- [x] M3 · 自定义块、变量、代码模式、设计稿剪贴板、动态变量节
|
|
683
|
+
- [ ] M4 · `@simple-mail/react` / `@simple-mail/vue` 适配器
|
|
684
|
+
- [ ] iframe 预览(Outlook ghost padding)
|
|
685
|
+
- [ ] 模板市场(EmailDoc JSON)
|
|
410
686
|
- [ ] i18n(zh-CN / en)
|
|
411
687
|
|
|
688
|
+
---
|
|
689
|
+
|
|
412
690
|
## 设计取舍
|
|
413
691
|
|
|
414
692
|
| 取舍点 | 选择 | 原因 |
|
|
415
|
-
|
|
416
|
-
|
|
|
417
|
-
|
|
|
418
|
-
|
|
|
419
|
-
|
|
|
420
|
-
|
|
|
421
|
-
| 包体 | gzip ≈ 580KB |
|
|
422
|
-
| 默认内边距 | Section 左右 16
|
|
693
|
+
|--------|------|------|
|
|
694
|
+
| 画布 HTML | 轻量预览 DOM | iframe 内 Sortable 难控;真实效果靠 MJML |
|
|
695
|
+
| 嵌套 | 严格四层 | 避免 GrapesJS 式邮件翻车 |
|
|
696
|
+
| 输出 | MJML 优先 | Outlook/Gmail 兼容;可预留 table 引擎 |
|
|
697
|
+
| 文档级源码回写 | 只读 | MJML→doc 反推风险大 |
|
|
698
|
+
| 状态 | immutable + history | 核心零 UI 框架依赖 |
|
|
699
|
+
| 包体 | gzip ≈ 580KB | 主因 mjml-browser |
|
|
700
|
+
| 默认内边距 | Section 左右 16;块上下 8 | 版心由节统一 |
|
|
701
|
+
|
|
702
|
+
---
|
|
423
703
|
|
|
424
704
|
## 许可证
|
|
425
705
|
|