定位

pf-editor 是图模工坊的设计端入口——管理员/设计师在浏览器里拖出版面、定义可编辑区域、保存为模板。它不是 PS 的替代品,而是面向”批量交付”的模板生产工具

  • 框架:Quasar 2.16 + Vue 3.5 + TypeScript 5.9
  • 渲染引擎:Leafer 2.x(开源高性能矢量渲染引擎)
  • 构建:Vite 7(quasar-cli-vite)
  • 端口:开发环境 9000
  • 鉴权:JWT(与 pf-manage 共用账号体系,走 pf-service 鉴权路由)

它和 PS / Figma 的区别

维度 PS / Figma pf-editor
输出 PSD / Fig 文件 sceneJson,可被代码消费
可编辑区域 靠图层命名约定 节点上显式 editable: true
字段化 用户在 PS 里改 用户在 H5 里填,编辑器侧只描述”哪里能改”
批量 手工另存 N 张 一份模板 + N 份用户填值 → N 张成品
人脸避让 设计师手动放区域 节点声明 faceSafe: true,合成时自动避让

源码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apps/pf-editor/src/
├── pages/
│ └── ErrorNotFound.vue
├── layouts/
├── components/
│ ├── PropertyEditor.vue # 右侧属性面板
│ ├── Upload.ts # 上传/拖拽逻辑
│ ├── editor/ # 主画布、工具栏、节点树
│ └── property-editor/ # 文字/图片/形状各自的属性子组件
├── composables/ # useEditor* / useHistory / useSelection ...
├── stores/ # Pinia 编辑器状态
├── api/ # 与 pf-service 的 HTTP 调用
├── boot/ # i18n、axios、router 守卫
└── ...

sceneJson 协议

整个编辑器围绕一份 JSON 工作。简化结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"version": "1",
"width": 800, // 模板物理宽(与 templates.width 同步)
"height": 600,
"background": "#ffffff",
"children": [
{
"id": "bg-1",
"tag": "Image",
"url": "https://.../bg.jpg",
"x": 0, "y": 0, "width": 800, "height": 600,
"locked": true // 用户端不可拖动
},
{
"id": "user-photo",
"tag": "Image",
"x": 100, "y": 80, "width": 600, "height": 400,
"editable": true, // 用户可换图
"placeholder": "请上传照片",
"faceSafe": true, // 自动避脸裁剪
"fit": "cover"
},
{
"id": "user-name",
"tag": "Text",
"x": 100, "y": 520, "width": 600, "height": 40,
"editable": true,
"text": "张三",
"fontFamily": "SourceHanSans",
"fontSize": 32,
"fill": "#222",
"align": "center"
}
]
}

节点关键属性

属性 类型 作用
id string 节点 id,用户提交时作为 userUploads / userTexts 的 elementKey
tag string Leafer 节点类型:Rect / Image / Text / Group / Path
x / y / width / height number 几何,单位与 templates.width/height 一致
editable boolean 是否允许终端用户修改(图片/文字)
locked boolean 编辑器侧是否锁定(防误改)
placeholder string 用户端未填值时的占位
faceSafe boolean 图片节点:合成时调用 face-service 做避脸裁剪
fit string 图片填充:cover / contain / fill
fontFamily / fontSize / fill / align mixed 文字属性,可由用户覆写

编辑能力

  • 基础节点:矩形、图片、文字、形状(Path)、组(Group)
  • 图层管理:节点树、层级调整、显隐、锁定
  • 画布操作:缩放(滚轮 / 触控板)、平移、对齐、吸附、网格
  • 历史记录:Undo / Redo(基于 sceneJson diff,存 stores)
  • 属性面板:右侧 PropertyEditor.vue 按选中节点 tag 切换子组件
  • 可编辑区标记:勾选 editable 后画布上以高亮虚线提示
  • 素材库联动:调用 pf-service/shared/background-library/shared/font-library 拉取共享素材
  • 测试合成:编辑过程中可调用一次”试合成”,验证最终效果(模板上线前的必跑校验)

与后端的交互

1
2
3
4
5
6
7
8
9
10
11
12
13
pf-editor
│ GET /pf-editor/templates ← 列表
│ GET /pf-editor/templates/:id ← 详情(含 sceneJson)
│ POST /pf-editor/templates ← 新建
│ PUT /pf-editor/templates/:id ← 保存
│ DELETE /pf-editor/templates/:id ← 软删

│ GET /shared/background-library ← 背景库
│ GET /shared/font-library ← 字体库
│ POST /shared/oss/upload (presign) ← 直传 MinIO


pf-service (Elysia)

路由实现位于 apps/pf-service/src/routes/projects/pf-editor/,按职责拆分:

  • templateEditorRoutes.ts — 模板增删改查、版本控制
  • templateSharedRoutes.ts — 与用户端、管理端共享的模板读接口

字体加载

编辑器和用户端共享 composables/fontPreload.ts——首次进入页面时按 font_variants 拉取并 FontFace.load() 注册到浏览器,确保画布渲染的文字与最终合成一致。

字体的”权威源”是 MinIO 中的 woff2 文件;Puppeteer 合成时通过相同 URL 注入,保证三端字形完全对齐。

协作约束

参见 apps/pf-app/dev.md 与项目根 AGENTS.md

  • Vue 文件 PascalCase,TS 文件 camelCase
  • <script setup lang="ts"> 强制
  • 组合式函数命名 useXxx,位于 composables/
  • 共享代码沉到 packages/*(如 @sa/hooks@sa/materials

与 pf-app 的代码复用

两个 Quasar 应用结构对称、Boot 文件几乎一致——核心组件如 ImageCropper.vueImagePreview.vue 也会通过 packages/materials 抽取共享。编辑器多出来的是”出版” UI,用户端多出来的是”消费” UI,渲染引擎和 sceneJson 解析逻辑是共用的,这是一份模板能跨端用的根本。

下一步