返回文章列表
/ 功能设计

邮件模板可视化编辑落地设计

用组件树编辑器拖拽编排邮件,产出 table 布局 + 内联样式 + 无 JS 的邮件安全 HTML。

邮件模板可视化编辑HTML

目标:用 runway(store 主题设计器 / jueying 组件树)作为邮件正文的「可视化编辑外壳」, 新建一套邮件组件集与**「组件树 → HTML 字符串」服务端渲染**,使商户可在可视化界面拖拽编排邮件, 同时保证产出的是邮件客户端安全的 HTML(table 布局 + 内联样式 + 无 JS)。


1. 背景与约束

1.1 现状问题

  • 邮件正文本是受限 JSX 源(htmlContent,含 {data.xxx} 与 .map 循环),由 babel 编译(renderEmail → renderEmailBody)。
  • 后台当前用 TinyMCE 编辑该 JSX 源,会破坏 JSX(把 {...} 当文本、归一化 class、丢失循环体),预览随之失效。
  • 纯代码编辑器(Monaco/CodeMirror)对非技术运营不友好。

1.2 邮件 HTML 的硬性约束(决定方案)

邮件客户端(尤其 Outlook 用 Word 引擎)要求:

  • table 布局 + 内联样式,禁止 position / flex / grid / 媒体查询 / :hover;
  • 无 JS、无 <script>、无 CSS 文件引用;
  • 样式必须内联到每个元素。

因此任何现有组件集都不能直接复用其渲染产物:

  • print 组件 → 打印/绝对定位 HTML(不适合邮件);
  • store(web) 组件 → antd / CSS-in-JS / 响应式(不适合邮件)。 两者都需新建「邮件版」组件。

1.3 runway 框架已具备的能力(可复用,不重新发明)

经代码核查,runway 与 print 同为 jueying 系组件树设计器,以下能力已现成:

  • ComponentData 树结构:{ type, props:{ id, children, ... } }(src/store/public/store-design/types.ts:52)。
  • field 数据绑定 + 数组循环:组件用 props.field 绑定数据字段,数组字段(如 items)逐行克隆展开(print 的 enterprise-sales-order.ts、order-ticket.test.ts 已验证)。
  • 示例数据预览:setPreviewData + 样例数据机制(print 设计页已落地)。
  • 服务端「组件树 → HTML 字符串」渲染管线:src/store/page-render/server-render/store-publish-page.ts:34-35 parsePageData(...) + ReactDOMServer.renderToString,并复用 jueying maishu-jueying/components-server 的 processTemplateString 做 ${...} 绑定。
  • 通用格式化工具:formatValue(value, format, locale)(src/print/shared/format.ts:61,支持 {c:CNY} 货币、{d} 日期),邮件侧可照搬一份到 src/common/email,避免 common→print 跨层耦合。
  • Node 端 SSR 能力:react-dom/server 已在用(src/print/client/render.ts:12),邮件渲染可直接 renderToStaticMarkup。

结论:框架层(拖拽 / 属性面板 / 持久化 / 示例数据 / 绑定循环 / SSR 管线思路)全部可复用; 唯一要新建的是邮件组件集与绑定后的字符串渲染器,且两者与「选哪套设计器」无关(print 仅作语义参考)。


2. 总体架构

商户可视化编辑                      后端存储                      发信
─────────────────                 ────────────                  ───────────
runway 设计器                       EmailTemplate 实体            mailer
(拖拽/属性面板)        ─保存─▶      contentData: ComponentData ──▶  renderEmailComponentData
(示例数据面板)                      sampleData: object              (ComponentData + 收件人数据
   │ 实时预览                                                       → HTML 字符串) ─▶ SMTP
   └──────── renderEmailComponentData(contentData, sampleData)
  • 现有 htmlContent(JSX 源)保留为系统内置模板兼容层;contentData 为商户可视化编辑通道。
  • mailer / 预览控制器按「有 contentData 优先,否则回落 renderEmail(htmlContent)」分支处理,现有 13 套系统模板零改动。

3. 数据模型

3.1 实体(EmailTemplate)

新增可选字段:

contentData?: ComponentData | null;   // 组件树(JSON 列),可视化编辑产物
// 已有:htmlContent, sampleData, subject, content, variables, ...
  • ComponentData 结构直接复用 jueying 定义(src/store/public/store-design/types.ts:52), 邮件侧在 src/common/email 复制一份轻量类型(避免 common→store 跨层依赖)。

3.2 示例数据(sampleData)

本次会话已在 EmailTemplate 实体、email-template-service、email-template-preview.controller、 后台 email/template/index.tsx 接通 sampleData(JSON,随模板保存、驱动实时预览)。 设计器「示例数据」面板即复用该字段作为收件人/订单样例。


4. 邮件组件集(src/common/email/components/)

纯 React 组件(无 antd / 无 window / 无 CSS-in-JS),render 一律输出 table + 内联样式:

组件用途关键 props
EmailSection区块容器(600px 居中外壳内的段落块)style
EmailText文本段field / text / format / style
EmailButton按钮(table+<a> 实现)field / text / href / color
EmailImage图片src / alt / width / srcField
EmailDivider分隔线style
EmailTable表格容器(承载循环行)style
EmailEach循环:props.field="items",子节点为行模板,渲染时按数组长度克隆并切换 item 作用域field
  • 组件支持 field 标量绑定(文本/属性),format 走本地 formatValue({c:CNY} / {d})。
  • 属性字段约定:srcField / hrefField 等以 Field 结尾的 prop 在渲染时解析为对应属性值。
  • 注册表 emailComponentTypes: Record<string, React.ComponentType>,供 loadEmailComponentTypes(服务端)使用。

设计器侧的「组件面板」即此集合,「属性面板」即各组件的属性编辑器(field / format / style 等)。


5. 绑定 + 循环引擎(src/common/email/render-component-data.ts)

递归遍历 ComponentData,产出 React element 树(叶子为 §4 邮件组件):

  1. 标量 field:value = field.split(".").reduce(scope) 点路径取值,{var} 文本插值复用 jueying processTemplateString 思路(自实现轻量版即可);format 经本地 formatValue 格式化。
  2. 数组 field(即 EmailEach):取数组 scope[field],克隆子节点 N 份,每份以 item 为作用域递归求值(print 的 tr[field=items] 逐行克隆同思路)。
  3. 属性字段(*Field):解析后写入对应 HTML 属性。
  4. 文本节点(string):做 {var} 插值。

输出:React element 树(其叶子是邮件组件)。


6. 服务端渲染(复用 store SSR 思路,去掉 JS 注入)

renderEmailComponentData(componentData: ComponentData, data: Record<string, unknown>): string {
  const element = evalNode(componentData, data);              // §5
  const body = ReactDOMServer.renderToStaticMarkup(element);  // 内联样式自动展开
  return wrapEmail(body, data);                               // 套 600px 居中外壳
}

对照 store-publish-page.ts:46-49:邮件不注入运行时 js / pageStyle link / <script>; 样式全部由组件内联。与现有 renderEmail(babel 编 JSX)并列,成为第二个渲染入口。


7. 设计器接入(复用 runway)

  • 新建邮件设计页(指向 runway 的 design-page 机制,src/store/public/store-design/design-page.tsx), 但切换数据源:
    • 组件面板 = §4 邮件组件集;
    • 属性面板 = 邮件组件编辑器(field / format 等);
    • 「示例数据」面板(本次已加)= 收件人/订单样例,实时预览用它渲染。
  • 保存 = ComponentData JSON 写回 contentData 字段。
  • 设计器画布是浏览器 DOM(近似样式),真实邮件效果以预览/发送时的字符串渲染为准,二者组件须一致。

8. 预览 / 保存 / 发送链路(最小侵入)

  • email-template-preview.controller:模板有 contentData → renderEmailComponentData(contentData, sampleData); 否则回落 renderEmail(htmlContent, ...)。
  • renderEmail / mailer:同样加「有 contentData 走新渲染,无则 JSX」分支。
  • 后台 email/template/index.tsx:编辑器入口从 TinyMCE 切换为 runway 设计器(或先保留代码编辑器、后续接设计器)。

9. 迁移策略

  • 系统内置 13 套:暂不转 ComponentData,继续 JSX renderEmail;mailer 优先 contentData,无则 JSX。
  • 新建 / 商户编辑:走 contentData 组件树。
  • 后续可选:提供「JSX 模板 → ComponentData」转换脚手架(非必须)。

10. 风险与注意

  • 邮件 HTML 规范:table + 内联样式;Outlook 禁 flex/grid,用 table+width。
  • 设计器画布(浏览器 DOM)与邮件字符串渲染需保持组件渲染一致,避免「设计器好看、收件箱错位」。
  • 现 JSX 模板的 .map 循环语义要能在 EmailEach 表达(items 数组)。
  • 跨层依赖:邮件组件/渲染器放 src/common/email,避免引入 store/print 的浏览器专用模块; formatValue 在 src/common/email 内自持一份(参考 src/print/shared/format.ts:61)。

11. 实施步骤(建议顺序)

  1. 在 src/common/email 定义轻量 ComponentData 类型(复用 jueying 结构)。
  2. 新建邮件组件集(src/common/email/components/)+ emailComponentTypes 注册表 + 本地 formatValue。
  3. 新建绑定/循环求值器(render-component-data.ts 的 evalNode / resolveValue / interpolate)。
  4. 新建 renderEmailComponentData(§6,含 wrapEmail 外壳)。
  5. 单测(render-component-data.test.ts):用 order 样例数据渲染,断言产出含 table + 内联 style + items 多行。
  6. 接入设计器(§7,前端)。
  7. 接入预览 / 保存 / 发送分支(§8)。
  8. 以 order 邮件为样板,跑通一个完整模板(文本字段 + EmailEach 商品列表 + 金额格式化)。