916 lines
26 KiB
Markdown
916 lines
26 KiB
Markdown
# Web Report Designer 初期架构设计方案 V1
|
||
|
||
> 文档状态:初期架构设计
|
||
>
|
||
> 本版本目标是先建立稳定的文档模型、布局管线和编辑器边界,不在第一阶段一次性实现所有复杂报表能力。
|
||
|
||
## 1. 项目定位
|
||
|
||
项目暂定名称:
|
||
|
||
~~~text
|
||
Web Report Designer
|
||
~~~
|
||
|
||
包名暂定:
|
||
|
||
~~~text
|
||
@xxx/report-core
|
||
@xxx/report-expression
|
||
@xxx/report-layout
|
||
@xxx/report-renderer
|
||
@xxx/report-designer
|
||
@xxx/report-designer-vue
|
||
~~~
|
||
|
||
项目定位:
|
||
|
||
> 一个面向业务系统的、纯 Web 的、可嵌入、可扩展、插件式的报表设计与渲染平台。
|
||
|
||
重点解决:
|
||
|
||
- 固定版式业务报表
|
||
- 打印单据
|
||
- A4/A5/自定义纸张
|
||
- 主表 + 子表
|
||
- 动态明细
|
||
- 多页打印
|
||
- 页眉页脚
|
||
- 基础分组和合计
|
||
- 数据绑定
|
||
- 受限表达式
|
||
- HTML、SVG 和浏览器打印
|
||
- 可扩展的 PDF 输出接口
|
||
|
||
明确不包含:
|
||
|
||
~~~text
|
||
BI
|
||
Dashboard
|
||
OLAP
|
||
数据分析
|
||
数据立方体
|
||
指标体系
|
||
大屏
|
||
~~~
|
||
|
||
### 1.1 V1 初期范围
|
||
|
||
V1 优先保证以下能力可用:
|
||
|
||
1. 文本、图片、线条、矩形等固定元素。
|
||
2. A4、A5 和自定义纸张。
|
||
3. 基础数据绑定和明细表。
|
||
4. 页眉、页脚和基础分页。
|
||
5. HTML、SVG 预览以及浏览器打印。
|
||
6. 文档序列化、校验和版本迁移。
|
||
7. 可注册的元素插件接口。
|
||
|
||
以下能力先预留模型和接口,放到后续迭代:
|
||
|
||
- 行列合并、跨页合计的分页级精细控制。
|
||
- 图表、条码、二维码等插件实现。
|
||
- 服务端 PDF 的具体实现。
|
||
- 协同编辑、权限体系和 BI 能力。
|
||
|
||
> 实现进度补充:基础分组(group-header / group-footer、多级分组、组内聚合)
|
||
> 已提前完成,详见 §16 阶段 3。
|
||
|
||
## 2. 总体架构
|
||
|
||
整体采用 Monorepo。
|
||
|
||
~~~text
|
||
report-designer/
|
||
│
|
||
├── apps/
|
||
│ └── report-designer/
|
||
│
|
||
├── packages/
|
||
│ ├── report-core/
|
||
│ ├── report-expression/
|
||
│ ├── report-layout/
|
||
│ ├── report-renderer/
|
||
│ │ └── src/
|
||
│ │ ├── html/
|
||
│ │ ├── svg/
|
||
│ │ └── pdf/
|
||
│ ├── report-designer/
|
||
│ ├── report-designer-vue/
|
||
│ └── plugins/
|
||
│ ├── qrcode/
|
||
│ ├── barcode/
|
||
│ ├── chart/
|
||
│ └── ...
|
||
│
|
||
├── examples/
|
||
├── pnpm-workspace.yaml
|
||
├── package.json
|
||
└── README.md
|
||
~~~
|
||
|
||
### 2.1 依赖方向
|
||
|
||
~~~text
|
||
report-expression ──> report-core
|
||
report-layout ──> report-core + report-expression
|
||
report-renderer ──> report-core + report-layout
|
||
report-designer ──> report-core + report-layout
|
||
report-designer-vue ──> report-designer
|
||
~~~
|
||
|
||
上图箭头表示“依赖”。Core 不依赖上述任何实现包。
|
||
|
||
Renderer 只消费布局结果,不重新计算分页。插件实现可以依赖 Core、Layout、Renderer 或 Designer 的适配器,但 Core 不反向依赖任何具体插件。
|
||
|
||
~~~text
|
||
plugin-qrcode
|
||
├── core element contract
|
||
├── layout adapter
|
||
├── renderer adapter
|
||
└── designer adapter(可选)
|
||
~~~
|
||
|
||
### 2.2 运行时主流程
|
||
|
||
~~~text
|
||
ReportDocument
|
||
│
|
||
▼
|
||
数据提供与参数注入
|
||
│
|
||
▼
|
||
表达式解析与绑定求值
|
||
│
|
||
▼
|
||
LayoutEngine(测量、展开、换行、分页)
|
||
│
|
||
▼
|
||
LayoutDocument(统一的分页结果)
|
||
│
|
||
├── HTML Renderer
|
||
├── SVG Renderer
|
||
└── PDF Renderer
|
||
~~~
|
||
|
||
## 3. 核心架构原则
|
||
|
||
### 3.1 Core 与 UI 完全分离
|
||
|
||
report-core:
|
||
|
||
~~~text
|
||
纯 TypeScript
|
||
无 Vue
|
||
无 React
|
||
无 DOM
|
||
无浏览器 API
|
||
无 UI 框架
|
||
~~~
|
||
|
||
Core 可以在 Browser、Node.js、Electron、Server 和 CLI 中复用。
|
||
|
||
### 3.2 文档模型与运行时状态分离
|
||
|
||
ReportDocument 只保存可序列化的报表定义,不保存以下编辑器状态:
|
||
|
||
- 当前选中项。
|
||
- 缩放比例和画布滚动位置。
|
||
- 辅助线、吸附线和临时选区。
|
||
- 撤销栈和 redo 栈。
|
||
- 已加载的数据和网络连接。
|
||
|
||
### 3.3 Layout 与 Renderer 分离
|
||
|
||
Layout 负责:
|
||
|
||
~~~text
|
||
尺寸计算
|
||
文本测量
|
||
换行
|
||
表格展开
|
||
动态高度
|
||
分页
|
||
~~~
|
||
|
||
Renderer 负责:
|
||
|
||
~~~text
|
||
把 LayoutDocument 绘制成 HTML、SVG 或 PDF
|
||
~~~
|
||
|
||
这样可以保证不同输出目标使用同一套分页结果。
|
||
|
||
### 3.4 运行时数据由外部注入
|
||
|
||
Core 不负责 fetch、数据库连接或 Token 管理。数据源定义和运行时数据提供器分开,避免把敏感配置写入报表文件。
|
||
|
||
### 3.5 文档必须可校验、可迁移
|
||
|
||
所有文档都必须带 schemaVersion,反序列化时先校验,再按版本逐步迁移。文档 JSON 中不能保存函数、DOM 节点或运行时对象。
|
||
|
||
## 4. 包职责
|
||
|
||
### 4.1 report-core
|
||
|
||
- 报表文档模型。
|
||
- 页面、区域、元素和样式模型。
|
||
- 数据源、数据集、参数契约。
|
||
- 插件契约和注册表。
|
||
- 文档创建、克隆、规范化。
|
||
- 序列化、反序列化、校验和迁移。
|
||
|
||
### 4.2 report-expression
|
||
|
||
- 表达式词法和语法解析。
|
||
- AST 和受限求值器。
|
||
- 字段、参数、行、分组和页面作用域。
|
||
- 表达式错误和诊断信息。
|
||
|
||
禁止通过 eval 或 new Function 执行任意 JavaScript。
|
||
|
||
### 4.3 report-layout
|
||
|
||
- 元素尺寸测量。
|
||
- 文本换行和动态高度。
|
||
- 明细数据展开。
|
||
- 分页和分页诊断。
|
||
- 生成统一的 LayoutDocument。
|
||
|
||
Layout 通过 TextMeasurer、FontResolver 等接口获得环境能力,不直接依赖 DOM。
|
||
|
||
### 4.4 report-renderer
|
||
|
||
- 消费 LayoutDocument。
|
||
- 提供 HTML、SVG 和 PDF Renderer。
|
||
- 处理目标格式相关的颜色、字体和资源输出。
|
||
|
||
PDF 的具体实现可以运行在浏览器或 Node/服务端,但必须遵循统一的 Renderer 接口。
|
||
|
||
### 4.5 report-designer
|
||
|
||
- 画布和坐标转换。
|
||
- 选择、拖拽、缩放、对齐和吸附。
|
||
- 属性编辑。
|
||
- 复制粘贴。
|
||
- 命令、事务和撤销重做。
|
||
- 快捷键和编辑器状态。
|
||
|
||
Designer 通过 Core 的命令接口修改文档,不直接实现报表业务规则。
|
||
|
||
### 4.6 report-designer-vue
|
||
|
||
只负责 Vue 组件、面板和 UI 适配,不向 Core 泄漏 Vue 类型。
|
||
|
||
## 5. report-core 目录
|
||
|
||
~~~text
|
||
packages/report-core/
|
||
│
|
||
├── src/
|
||
│ ├── model/
|
||
│ │ ├── report.ts
|
||
│ │ ├── page.ts
|
||
│ │ ├── section.ts
|
||
│ │ ├── element.ts
|
||
│ │ ├── table.ts
|
||
│ │ ├── style.ts
|
||
│ │ ├── dataset.ts
|
||
│ │ ├── datasource.ts
|
||
│ │ ├── parameter.ts
|
||
│ │ └── common.ts
|
||
│ │
|
||
│ ├── plugin/
|
||
│ │ ├── plugin.ts
|
||
│ │ ├── context.ts
|
||
│ │ ├── registry.ts
|
||
│ │ ├── element-plugin.ts
|
||
│ │ ├── datasource-plugin.ts
|
||
│ │ └── exporter-plugin.ts
|
||
│ │
|
||
│ ├── document/
|
||
│ │ ├── create.ts
|
||
│ │ ├── clone.ts
|
||
│ │ └── normalize.ts
|
||
│ │
|
||
│ ├── serialization/
|
||
│ │ ├── serialize.ts
|
||
│ │ ├── deserialize.ts
|
||
│ │ └── migrate.ts
|
||
│ │
|
||
│ ├── schema/
|
||
│ │ ├── version.ts
|
||
│ │ ├── defaults.ts
|
||
│ │ └── validation.ts
|
||
│ │
|
||
│ ├── diagnostics/
|
||
│ │ └── diagnostic.ts
|
||
│ ├── utils/
|
||
│ │ ├── id.ts
|
||
│ │ └── deep.ts
|
||
│ └── index.ts
|
||
│
|
||
└── tests/
|
||
~~~
|
||
|
||
## 6. Report 文档模型
|
||
|
||
报表采用“文档 + 区域 + 元素树”模型,不把所有元素平铺在根节点上。
|
||
|
||
~~~ts
|
||
export interface ReportDocument {
|
||
schemaVersion: number
|
||
|
||
id: string
|
||
name: string
|
||
|
||
page: PageSettings
|
||
|
||
styles: Record<string, StyleDefinition>
|
||
|
||
dataSources: Record<string, DataSourceDefinition>
|
||
datasets: Record<string, DatasetDefinition>
|
||
parameters: Record<string, ReportParameter>
|
||
|
||
sections: ReportSection[]
|
||
|
||
plugins?: ReportPluginReference[]
|
||
metadata?: ReportMetadata
|
||
}
|
||
~~~
|
||
|
||
### 6.1 报表区域
|
||
|
||
~~~ts
|
||
export type ReportSectionKind =
|
||
| "report-header"
|
||
| "page-header"
|
||
| "detail"
|
||
| "group-header"
|
||
| "group-footer"
|
||
| "summary"
|
||
| "page-footer"
|
||
|
||
export interface ReportSection {
|
||
id: string
|
||
kind: ReportSectionKind
|
||
|
||
datasetId?: string
|
||
children: ReportElement[]
|
||
|
||
options?: {
|
||
repeatOnEachPage?: boolean
|
||
keepTogether?: boolean
|
||
allowSplit?: boolean
|
||
}
|
||
}
|
||
~~~
|
||
|
||
V1 先实现 page-header、detail 和 page-footer,其他区域只保留模型扩展点。
|
||
|
||
> 实现进度:group-header / group-footer 已可用(含多级分组与组内聚合)。
|
||
> report-header 与 summary 仍只保留模型扩展点。
|
||
>
|
||
> 分组区域通过 `groupExpression` 指定分组键,`groupLevel` 指定嵌套级别:
|
||
>
|
||
> ~~~ts
|
||
> export interface ReportSection {
|
||
> // ...原有字段
|
||
> /** 分组键表达式,仅 group-header / group-footer 使用。 */
|
||
> groupExpression?: string
|
||
> /** 分组级别,从 1 开始;数值小的为外层组。 */
|
||
> groupLevel?: number
|
||
> }
|
||
> ~~~
|
||
>
|
||
> 分组语义为「**相邻行键值相同即同组**」,不改变行顺序——
|
||
> 因此宿主应先按分组键排序再传入数据。组脚可通过 `group` 作用域访问
|
||
> 聚合能力,如 `sum(group.amount)`、`group.count`、`group.key`。
|
||
|
||
### 6.2 元素树
|
||
|
||
~~~ts
|
||
export interface ReportElement {
|
||
id: string
|
||
type: string
|
||
|
||
frame: Rect
|
||
styleId?: string
|
||
|
||
binding?: ValueBinding
|
||
props?: Record<string, unknown>
|
||
children?: ReportElement[]
|
||
}
|
||
|
||
export interface Rect {
|
||
x: number
|
||
y: number
|
||
width: number
|
||
height: number
|
||
}
|
||
|
||
export interface ValueBinding {
|
||
field?: string
|
||
expression?: string
|
||
format?: string
|
||
}
|
||
~~~
|
||
|
||
内置元素初期包括:
|
||
|
||
~~~text
|
||
text
|
||
image
|
||
line
|
||
rect
|
||
container
|
||
table
|
||
~~~
|
||
|
||
插件元素使用自定义 type,并通过插件 Schema 验证 props。
|
||
|
||
### 6.3 明细表
|
||
|
||
明细表是 V1 的特殊元素,负责把数据集展开成多行。单元格内的元素坐标相对于当前单元格。
|
||
|
||
~~~ts
|
||
export interface TableElement extends ReportElement {
|
||
type: "table"
|
||
datasetId: string
|
||
columns: TableColumn[]
|
||
repeatHeaderOnEachPage?: boolean
|
||
}
|
||
|
||
export interface TableColumn {
|
||
id: string
|
||
width: number
|
||
header?: string
|
||
binding?: ValueBinding
|
||
children?: ReportElement[]
|
||
}
|
||
~~~
|
||
|
||
### 6.4 样式
|
||
|
||
样式通过 ID 复用,并允许有限的继承和元素级覆盖:
|
||
|
||
~~~ts
|
||
export interface StyleDefinition {
|
||
id: string
|
||
extends?: string
|
||
fontFamily?: string
|
||
fontSize?: number
|
||
color?: string
|
||
background?: string
|
||
border?: BorderStyle
|
||
align?: "left" | "center" | "right"
|
||
verticalAlign?: "top" | "middle" | "bottom"
|
||
}
|
||
~~~
|
||
|
||
## 7. Page 模型
|
||
|
||
V1 文档内部统一使用 mm,px 只用于 Designer 显示层,pt 只在 PDF 等输出边界转换。
|
||
|
||
~~~ts
|
||
export interface PageSettings {
|
||
paper: "A4" | "A5" | "custom"
|
||
orientation: "portrait" | "landscape"
|
||
|
||
customSize?: {
|
||
width: number
|
||
height: number
|
||
}
|
||
|
||
margin: PageMargin
|
||
background?: string
|
||
}
|
||
|
||
export interface PageMargin {
|
||
top: number
|
||
right: number
|
||
bottom: number
|
||
left: number
|
||
}
|
||
~~~
|
||
|
||
paper 和 orientation 是页面配置来源;Layout 阶段将其解析为最终的 width、height。自定义纸张必须提供尺寸,尺寸均以 mm 表示。
|
||
|
||
## 8. 数据和运行时上下文
|
||
|
||
报表文档只保存数据源定义,不保存连接、Token 和本次查询结果。
|
||
|
||
~~~ts
|
||
export interface DataSourceDefinition {
|
||
id: string
|
||
type: string
|
||
config?: Record<string, unknown>
|
||
connectionRef?: string
|
||
}
|
||
|
||
export interface DatasetDefinition {
|
||
id: string
|
||
sourceId?: string
|
||
fields?: DatasetField[]
|
||
query?: string
|
||
}
|
||
|
||
export interface RenderContext {
|
||
data: Record<string, unknown>
|
||
parameters: Record<string, unknown>
|
||
locale?: string
|
||
timezone?: string
|
||
}
|
||
|
||
export interface DataProvider {
|
||
getDataset(
|
||
dataset: DatasetDefinition,
|
||
context: RenderContext
|
||
): Promise<ReadonlyArray<Record<string, unknown>>>
|
||
}
|
||
~~~
|
||
|
||
DataProvider 由宿主应用注入,Core 不主动访问网络或数据库。
|
||
config 只保存非敏感的设计配置;连接密码、Token 等敏感信息必须通过 connectionRef 或宿主运行时注入。
|
||
|
||
## 9. Layout 中间结果
|
||
|
||
Layout Engine 接受报表文档和运行时上下文,输出所有 Renderer 共用的分页结果。
|
||
|
||
~~~ts
|
||
export interface LayoutOptions {
|
||
dataProvider?: DataProvider
|
||
textMeasurer: TextMeasurer
|
||
fontResolver: FontResolver
|
||
}
|
||
|
||
export interface LayoutEngine {
|
||
layout(
|
||
document: ReportDocument,
|
||
context: RenderContext,
|
||
options?: LayoutOptions
|
||
): Promise<LayoutDocument>
|
||
}
|
||
|
||
export interface LayoutDocument {
|
||
pages: LayoutPage[]
|
||
diagnostics: LayoutDiagnostic[]
|
||
}
|
||
|
||
export interface LayoutPage {
|
||
width: number
|
||
height: number
|
||
nodes: LayoutNode[]
|
||
}
|
||
|
||
export interface LayoutNode {
|
||
id: string
|
||
type: string
|
||
frame: Rect
|
||
style: ResolvedStyle
|
||
content?: unknown
|
||
children?: LayoutNode[]
|
||
}
|
||
~~~
|
||
|
||
Layout 必须支持以下接口:
|
||
|
||
~~~ts
|
||
export interface TextMeasurer {
|
||
measure(text: string, style: ResolvedStyle): TextMetrics
|
||
}
|
||
|
||
export interface FontResolver {
|
||
resolve(fontFamily: string): ResolvedFont
|
||
}
|
||
~~~
|
||
|
||
这样可以分别适配浏览器、Node 和服务端字体环境,同时让分页逻辑保持独立。
|
||
|
||
## 10. 表达式约定
|
||
|
||
表达式系统使用受限语法和 AST,不执行任意 JavaScript。
|
||
|
||
V1 至少支持:
|
||
|
||
~~~text
|
||
字段:row.amount
|
||
参数:params.customerName
|
||
简单运算:row.price * row.quantity
|
||
条件:if(row.amount > 0, "有", "无")
|
||
格式化:format(row.date, "YYYY-MM-DD")
|
||
~~~
|
||
|
||
表达式失败时返回带有元素 ID 和表达式内容的诊断信息,单个字段错误不应导致整个文档无结果。
|
||
|
||
## 11. 插件模型
|
||
|
||
插件只通过契约接入,不允许 Core 直接导入插件实现。
|
||
|
||
~~~ts
|
||
export interface ReportPlugin {
|
||
id: string
|
||
version: string
|
||
elements?: ElementPlugin[]
|
||
datasources?: DataSourcePlugin[]
|
||
exporters?: ExporterPlugin[]
|
||
}
|
||
|
||
export interface ElementPlugin {
|
||
type: string
|
||
schema: unknown
|
||
createDefault(): ReportElement
|
||
}
|
||
~~~
|
||
|
||
需要针对不同运行环境提供可选适配器:
|
||
|
||
~~~text
|
||
ElementPlugin
|
||
├── LayoutAdapter
|
||
├── RendererAdapter(html/svg/pdf)
|
||
└── DesignerAdapter
|
||
~~~
|
||
|
||
插件引用只保存 id、version 和配置。插件缺失时应产生诊断信息,而不是阻止整个文档被读取。
|
||
|
||
## 12. 序列化、校验和迁移
|
||
|
||
- schemaVersion 使用单调递增整数。
|
||
- 反序列化顺序为:解析 JSON → 校验结构 → 执行迁移 → 规范化默认值。
|
||
- 迁移函数按版本逐步执行,不允许跨版本直接修改内部对象。
|
||
- 序列化结果保持稳定排序,便于版本控制和差异比较。
|
||
- 校验错误必须指出字段路径和元素 ID。
|
||
|
||
示例:
|
||
|
||
~~~text
|
||
deserialize(json)
|
||
→ validate(json)
|
||
→ migrate(json, currentVersion)
|
||
→ normalize(document)
|
||
~~~
|
||
|
||
## 13. 诊断和错误处理
|
||
|
||
统一使用诊断对象表达警告和错误:
|
||
|
||
~~~ts
|
||
export interface Diagnostic {
|
||
severity: "info" | "warning" | "error"
|
||
code: string
|
||
message: string
|
||
path?: string
|
||
elementId?: string
|
||
}
|
||
~~~
|
||
|
||
数据缺失、表达式错误、字体缺失、插件缺失和分页失败都应尽量返回可定位的诊断信息。
|
||
|
||
## 14. UI 规范
|
||
|
||
### 14.1 设计目标
|
||
|
||
Designer 的界面采用现代、简洁、干净的工具型产品风格,参考 shadcn/ui 的设计语言,但不要求 Core 或业务应用必须绑定某一个 UI 框架。
|
||
|
||
核心原则:
|
||
|
||
- 画布优先,减少非必要装饰。
|
||
- 使用中性背景、细边框和低强度阴影建立层次。
|
||
- 通过间距、字号和颜色层级表达信息,不依赖渐变和大面积高亮。
|
||
- 常用操作保持短路径,复杂配置分组收起。
|
||
- 所有颜色、间距、圆角和阴影使用设计令牌,禁止组件内散落硬编码。
|
||
|
||
### 14.2 UI 层边界
|
||
|
||
- report-core 不包含任何 UI、CSS 或主题代码。
|
||
- report-designer 负责编辑器状态、命令和交互行为。
|
||
- report-designer-vue 负责 Vue 组件、面板和样式实现。
|
||
- 组件采用 Headless 思路,宿主应用可以覆盖主题和品牌色。
|
||
- 可以使用 shadcn-vue 或同类组件实现,但必须通过 report-designer-vue 隔离依赖。
|
||
|
||
### 14.3 整体布局
|
||
|
||
桌面端采用三栏、画布居中的工作区:
|
||
|
||
~~~text
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 顶部工具栏:文件、撤销、重做、预览、保存、缩放、主题 │
|
||
├──────────────┬───────────────────────────────┬───────────────┤
|
||
│ 左侧工具箱 │ │ 右侧属性面板 │
|
||
│ 元素 / 数据 │ 报表画布 │ 属性 / 样式 │
|
||
│ 模板 / 图层 │ │ 数据绑定 │
|
||
├──────────────┴───────────────────────────────┴───────────────┤
|
||
│ 底部状态栏:页码、缩放、单位、布局诊断、快捷键提示 │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
~~~
|
||
|
||
建议尺寸:
|
||
|
||
- 左侧工具箱:240px 左右,可折叠。
|
||
- 右侧属性面板:320px 左右,可折叠。
|
||
- 顶部工具栏:48px。
|
||
- 底部状态栏:28px。
|
||
- 中间画布区域自适应,并支持拖拽调整两侧面板宽度。
|
||
|
||
屏幕宽度小于 900px 时,侧栏改为抽屉或浮层;移动端优先保证预览和基础属性编辑,不强行复刻完整桌面编辑器。
|
||
|
||
### 14.4 设计令牌
|
||
|
||
使用 CSS 自定义属性作为主题契约,至少提供亮色和暗色两套主题:
|
||
|
||
~~~css
|
||
:root {
|
||
--rd-background: 0 0% 100%;
|
||
--rd-foreground: 222 47% 11%;
|
||
--rd-muted: 210 40% 96%;
|
||
--rd-muted-foreground: 215 16% 47%;
|
||
--rd-border: 214 32% 91%;
|
||
--rd-input: 214 32% 91%;
|
||
--rd-primary: 221 83% 53%;
|
||
--rd-primary-foreground: 210 40% 98%;
|
||
--rd-destructive: 0 72% 51%;
|
||
--rd-canvas: 220 14% 96%;
|
||
--rd-selection: 221 83% 53%;
|
||
--rd-radius: 8px;
|
||
}
|
||
~~~
|
||
|
||
具体色值可以由宿主应用覆盖,但必须保持以下语义:
|
||
|
||
- background:应用和面板背景。
|
||
- canvas:画布工作区背景。
|
||
- foreground:主要文字和图标。
|
||
- muted:次级区域和禁用背景。
|
||
- border:分隔线和输入框边框。
|
||
- primary:主操作、选中态和焦点环。
|
||
- destructive:删除和不可逆操作。
|
||
- selection:画布元素选中框和辅助线。
|
||
|
||
推荐使用 OKLCH 或 HSL 变量,不在组件中直接写十六进制颜色。
|
||
|
||
### 14.5 字体和间距
|
||
|
||
~~~css
|
||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||
"PingFang SC", "Microsoft YaHei", sans-serif;
|
||
~~~
|
||
|
||
推荐字号:12px(辅助信息)、13px(表单和工具栏)、14px(正文)、16px(小标题)、20px(页面标题)。
|
||
|
||
间距使用 4px 基准:4、8、12、16、20、24、32px。默认圆角 6~8px,面板阴影保持低强度,避免卡片堆叠感。
|
||
|
||
### 14.6 基础组件风格
|
||
|
||
基础组件遵循扁平、克制、可聚焦的风格:
|
||
|
||
- Button:Primary、Secondary、Ghost、Destructive、Icon 五类。
|
||
- Input、Textarea、Select、Combobox:1px 边框,聚焦时显示明显但克制的 focus ring。
|
||
- Tabs、Segmented Control:用于属性面板和视图切换。
|
||
- Popover、Tooltip、Context Menu、Command Menu:用于低频和快捷操作。
|
||
- Dialog、Toast、Alert:用于确认、反馈和错误提示。
|
||
- Resizable Panel:用于左右侧栏和属性面板。
|
||
|
||
按钮不使用过度圆润的胶囊形,图标按钮必须提供 Tooltip 和可访问名称。删除等危险操作不能只依赖颜色区分。
|
||
|
||
### 14.7 画布规范
|
||
|
||
- 工作区使用浅灰或深灰中性背景,页面使用白色或主题纸张颜色。
|
||
- 页面保持轻微阴影和清晰边界,不使用强烈投影。
|
||
- 元素 Hover 使用低透明度背景,Selected 使用 1px 主色边框和 8 个缩放控制点。
|
||
- 对齐线、吸附线和标尺使用辅助色,不遮挡元素内容。
|
||
- 支持 50%、75%、100%、125%、150%、200% 常用缩放。
|
||
- 画布操作同时支持鼠标、键盘和上下文菜单,不把拖拽作为唯一入口。
|
||
- 选中态、错误态和辅助线不能只通过颜色表达,必要时增加边框、图标或文字提示。
|
||
|
||
### 14.8 属性面板规范
|
||
|
||
- 按“布局、内容、样式、数据绑定、高级”分组。
|
||
- 分组默认折叠低频配置,保留最近使用状态。
|
||
- 数值输入显示单位,支持键盘微调和批量修改。
|
||
- 绑定字段提供字段选择器,同时允许直接输入表达式。
|
||
- 无选中元素时显示页面属性;多选时只显示共同属性。
|
||
- 表单错误就地显示,并关联到对应字段,不使用只在底部出现的泛化错误。
|
||
|
||
### 14.9 交互和状态
|
||
|
||
顶部工具栏至少提供:
|
||
|
||
- 新建、打开、保存。
|
||
- 撤销、重做。
|
||
- 预览、打印。
|
||
- 缩放和适应页面。
|
||
- 主题切换和帮助入口。
|
||
|
||
推荐快捷键:Ctrl/Cmd + S 保存、Ctrl/Cmd + Z 撤销、Ctrl/Cmd + Shift + Z 重做、Delete 删除、Ctrl/Cmd + C/V 复制粘贴、Ctrl/Cmd + K 打开命令菜单。
|
||
|
||
所有异步操作都要有 Loading、Empty、Error 和 Disabled 状态;保存成功、布局警告和表达式错误使用统一 Toast 或诊断面板反馈。
|
||
|
||
### 14.10 主题、密度和无障碍
|
||
|
||
- 默认提供 Light 和 Dark 两套主题。
|
||
- 支持 Standard 和 Compact 两种密度,默认 Standard。
|
||
- 颜色对比度目标达到 WCAG AA,正文至少 4.5:1。
|
||
- 所有交互控件支持键盘访问,焦点状态必须可见。
|
||
- 弹窗、抽屉、菜单和属性面板提供正确的 ARIA 语义。
|
||
- 点击目标不小于 32px,主要触控目标建议不小于 36px。
|
||
- 尊重 prefers-reduced-motion,避免非必要动画。
|
||
|
||
### 14.11 UI 验收标准
|
||
|
||
初期 UI 至少应满足:
|
||
|
||
1. 用户可以在不阅读文档的情况下找到新增元素、保存、预览和撤销操作。
|
||
2. 画布、工具箱和属性面板在亮色和暗色主题下都能正常使用。
|
||
3. 选中、禁用、错误、加载和空状态具有一致的视觉语言。
|
||
4. 关键操作可以只使用键盘完成。
|
||
5. 主题颜色可以由宿主应用通过 CSS 变量覆盖。
|
||
|
||
## 15. 测试策略
|
||
|
||
初期至少建立以下测试:
|
||
|
||
- √ 文档 Schema、默认值和版本迁移测试。
|
||
- √ 固定元素和明细表的布局快照测试。
|
||
- √ 页眉、页脚和多页分页测试。
|
||
- HTML、SVG、PDF Renderer 的公共布局结果一致性测试。
|
||
- √ 表达式安全性、空值和类型转换测试。
|
||
- √ 中文字体、长文本和自定义纸张测试。
|
||
- √ 插件缺失和插件版本不兼容测试。
|
||
|
||
建议准备一个固定的示例报表作为跨包契约测试数据。
|
||
|
||
> 当前测试规模:`report-core` 139 例、`report-expression` 111 例、
|
||
> `report-layout` 124 例、`report-renderer` 12 例、`report-designer` 14 例、
|
||
> `report-designer-vue` 11 例,合计 **411** 例。
|
||
> 字体相关用例依赖系统字体,在缺少字体的环境中自动跳过。
|
||
> HTML/SVG 分页一致性测试已随 Renderer 实现补齐。
|
||
|
||
## 16. 初期实施顺序
|
||
|
||
> 进度标记说明:`√` 表示已实现并有测试覆盖;`◐` 表示部分实现;未标记表示尚未开始。
|
||
>
|
||
> 当前进度:阶段 1、2、3 已完成;阶段 4 主体完成(Renderer、Designer 核心、
|
||
> Vue UI 骨架),Vue 层的图层面板、数据绑定面板、快捷键留待后续迭代。
|
||
|
||
### √ 阶段 1:Core 和文档模型
|
||
|
||
- √ 完成 ReportDocument、Section、Element、Page、Style。
|
||
- √ 完成默认值、校验、序列化和迁移。
|
||
- √ 完成基础命令和 ID 生成。
|
||
|
||
产物:`packages/report-core`(139 个测试用例)。
|
||
|
||
### √ 阶段 2:基础 Layout
|
||
|
||
- √ 完成单页固定元素布局。
|
||
- √ 完成文本测量、换行和基础分页。
|
||
- √ 输出统一的 LayoutDocument。
|
||
|
||
产物:`packages/report-layout`(含自研 TTF/TTC 字体度量解析)。
|
||
|
||
### √ 阶段 3:数据和明细表
|
||
|
||
- √ 接入 RenderContext 和 DataProvider。
|
||
- √ 支持字段绑定、简单表达式和明细表展开。
|
||
- √ 支持页眉、页脚重复。
|
||
|
||
产物:`packages/report-expression`(83+28 个测试用例)、`packages/report-layout` 的
|
||
数据绑定与分组能力。
|
||
|
||
补充实现(超出本阶段原始列项):
|
||
|
||
- √ 受限表达式系统(字段、参数、运算、条件、格式化)。
|
||
- √ 聚合函数(sum / avg / count / min / max)。
|
||
- √ 基础分组(group-header / group-footer、多级分组、组内聚合)。
|
||
- √ 明细表表头每页重复。
|
||
|
||
### ◐ 阶段 4:Renderer 和 Designer
|
||
|
||
- √ 实现 SVG、HTML 和浏览器打印(`@report/renderer`,含 HTML/SVG 分页一致性测试)。
|
||
- √ 实现 PDF Renderer 契约(可注入 `PdfBackend`,缺失/报错走诊断)。
|
||
- √ 完成 Designer 的选择、拖拽、属性编辑和撤销重做(`@report/designer`,纯逻辑层)。
|
||
- √ Vue 层 UI 骨架(`@report/designer-vue`,原生 Vue3 + Vite 库模式,无第三方组件库):
|
||
三栏布局壳、画布(渲染/选中/新增)、工具栏(撤销重做/缩放)、属性面板(基础编辑)、
|
||
状态栏、设计令牌(亮/暗主题 CSS 变量)。
|
||
- 待后续迭代:图层面板、数据绑定面板、拖拽移动、快捷键、预览接入 Renderer。
|
||
|
||
### 阶段 5:插件和 PDF
|
||
|
||
- 实现二维码、条码等独立插件。
|
||
- 根据部署环境确定 PDF 的具体后端。
|
||
- 补充字体、资源和输出一致性测试。
|
||
|
||
## 17. V1 验收标准
|
||
|
||
一个示例报表应当能够:
|
||
|
||
1. 在 Designer 中创建并保存。
|
||
2. 绑定一组运行时数据。
|
||
3. 生成包含页眉、明细和页脚的多页结果。
|
||
4. 在 HTML 和 SVG 中得到相同的分页结构。
|
||
5. 通过浏览器打印输出。
|
||
6. 在升级 Schema 后仍可被迁移和读取。
|
||
|
||
达到以上标准后,再继续扩展复杂分组、图表、PDF 和更多插件。
|