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