Files
workspace/code/one-designer/packages/report-core
2026-09-20 17:34:40 +08:00
..
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00

@report/core

报表文档模型、插件契约、校验、序列化与迁移。这是 Web Report Designer 的最底层包。

对应设计文档 §4.1、§5、§6、§7、§12、§13。

定位

@report/core 是纯 TypeScript 包:无 Vue / React、无 DOM、无浏览器 API、无 UI 框架。 因此它可以在 Browser、Node.js、Electron、Server 和 CLI 中复用(架构原则 §3.1)。

Core 不依赖任何上层实现包,也不反向依赖任何具体插件。

安装与使用

pnpm add @report/core
import {
  createDocument,
  serialize,
  deserialize,
  createEditSession,
  createCommand,
  resolvePageSize,
} from "@report/core"

// 新建文档
const doc = createDocument({ name: "销售单", page: { paper: "A4" } })

// 解析纸张尺寸(mm)
const size = resolvePageSize(doc.page) // { width: 210, height: 297 }

// 序列化(稳定排序,便于版本控制 diff)
const json = serialize(doc, { pretty: true })

// 反序列化:parse → validate → migrate → normalize
const result = deserialize(json)
if (result.ok) {
  console.log(result.document?.name)
}
console.log(result.diagnostics)

核心概念

文档模型:文档 → 区域 → 元素树

不把所有元素平铺在根节点上。区域(ReportSection)有 7 种 kind,V1 只实现 page-header / detail / page-footer,其余保留模型扩展点。

单位约定

文档内部统一使用 mm;px 只用于 Designer 显示层,pt 只在 PDF 等输出边界转换(§7)。 paper + orientation 是配置来源,由 resolvePageSize 在布局阶段解析为最终 width/height。

纯数据约束

ReportDocument 只保存可序列化的报表定义,不保存编辑器状态(§3.2): 当前选中项、缩放比例、滚动位置、辅助线、撤销栈、已加载数据和网络连接均不得进入文档。 serialize 会在规范化之前先断言纯数据性,混入函数或 DOM 节点会直接抛 TypeError。

诊断而非异常

数据缺失、表达式错误、字体缺失、插件缺失、分页失败都通过 Diagnostic 表达, 并尽量带上 path(JSON Pointer 风格字段路径)与 elementId,不中断文档读取(§13)。

import { DIAGNOSTIC_CODES, DiagnosticBag, formatDiagnostic } from "@report/core"

const bag = new DiagnosticBag()
bag.warn(DIAGNOSTIC_CODES.REF_DANGLING_DATASET, "区域引用了不存在的数据集", {
  path: "/sections/1/datasetId",
})
console.log(bag.all().map(formatDiagnostic))

序列化与迁移流程(§12)

deserialize(json)
  → parse     解析 JSON
  → validate  校验结构
  → migrate   按版本逐步迁移
  → normalize 规范化默认值
  • schemaVersion 使用单调递增整数,当前为 CURRENT_SCHEMA_VERSION。
  • 迁移函数按版本逐步执行,planMigrations 会强制 to === from + 1,禁止跨版本跳跃。
  • 序列化结果按键名稳定排序,便于 diff。
  • 版本高于当前支持时拒绝读取,避免降级写坏数据。

新增 schema 版本时,在 src/schema/version.ts 的 MIGRATIONS 中追加一项即可。

命令与撤销重做

Designer 通过 Core 的命令接口修改文档,不直接实现报表业务规则(§4.5)。 撤销栈属于编辑器状态,不进入文档(§3.2),因此 History 由调用方持有。

import { createCommand, createEditSession } from "@report/core"

const rename = createCommand<{ name: string }>("rename", (doc, payload) => {
  const next = JSON.parse(JSON.stringify(doc))
  next.name = payload.name
  return next
})

const session = createEditSession(createDocument({ name: "A" }))
session.execute(rename, { name: "B" })
session.undo() // 回到 "A"
session.redo() // 回到 "B"

// 事务:多次编辑合并为一步撤销
session.transaction("批量调整", (s) => {
  s.applyWithinTransaction(rename, { name: "C" })
  s.applyWithinTransaction(rename, { name: "D" })
})

撤销采用快照式而非反向命令式:文档是纯数据且体量可控,快照成本可接受, 且天然正确处理「一个事务改了多处」的情况。mergeKey + mergeWindow 用于合并 拖拽、连续输入这类高频操作。

插件

插件只通过契约接入,Core 不导入任何具体插件实现(§11)。 插件缺失或版本不兼容只产生诊断,不阻止文档读取。

import { PluginRegistry } from "@report/core"

const registry = new PluginRegistry()
registry.register({
  id: "plugin-qrcode",
  version: "1.2.0",
  compatibleSchemaVersion: ">=1.0.0",
  elements: [
    {
      type: "qrcode",
      createDefault: (options) => ({
        id: options?.id ?? "el-1",
        type: "qrcode",
        frame: options?.frame ?? { x: 0, y: 0, width: 20, height: 20 },
        props: { value: "https://example.com" },
      }),
    },
  ],
})

// 校验文档时传入插件类型集合,避免误报未知元素类型
const result = deserialize(json, { pluginElementTypes: registry.elementTypes() })

ElementPlugin.schema 使用 unknown 是有意为之:Core 不绑定任何具体校验库, 宿主可以传 JSON Schema、Zod、Valibot 或自定义描述,由插件适配器负责具体校验。

目录结构

src/
├── model/          报表文档模型(report / page / section / element / table / style / …)
├── plugin/         插件契约与注册表
├── document/       创建、克隆、规范化
├── serialization/  序列化、反序列化、迁移
├── schema/         版本、默认值、校验
├── command/        命令、事务、撤销重做
├── diagnostics/    统一诊断对象
├── utils/          ID 生成、深拷贝与对象工具
└── index.ts        公共出口

开发

pnpm build       # tsup 打包(ESM + CJS + d.ts)
pnpm test        # vitest
pnpm typecheck   # tsc --noEmit

当前测试覆盖

测试文件 用例数 覆盖内容
document.test.ts 19 创建、规范化、克隆、元素计数
serialization.test.ts 26 稳定序列化、往返、迁移链路、容错读取
validation.test.ts 34 结构校验、引用完整性、样式环、插件类型
command.test.ts 35 命令、合并、事务、撤销重做
plugin.test.ts 25 注册、冲突、版本兼容、缺失诊断

合计 139 个用例。

V1 范围内尚未实现的部分

本包只覆盖设计文档 §16 的阶段 1(Core 和文档模型)。以下属于后续阶段:

  • report-expression:表达式解析与受限求值器(阶段 3)。
  • report-layout:测量、换行、分页,产出 LayoutDocument(阶段 2)。
  • report-renderer:HTML / SVG / PDF(阶段 4)。
  • report-designer / report-designer-vue:编辑器与 UI(阶段 4)。
  • 插件实现:二维码、条码、图表(阶段 5)。