ReportView 与 Report Workspace
通用布局能力:本页中的可布局
Render*视图和工作区实例统一支持width、height、min/max、margin和槽位对齐;继承RenderStackPanel的工作区还支持容器padding。详见组件通用布局属性。
报表系统采用 Cell-first、Relation-driven 模板。完整设计体验使用 RenderReportWorkspace;只展示已物化结果时使用 RenderReportRuntimeSurface;需要直接渲染通用虚拟表格时使用 RenderReportView 和 ReportVirtualModel。
旧的 Region-first 设计器与工作台已停止公开,不应在新代码中使用。
API 总览
| API | 类型 | 用途 |
|---|---|---|
RenderReportWorkspace |
class | schema 3 设计/预览工作区;显式 canary policy 下也可承载 schema 4 Authoring 和 Working Preview。 |
workspace.documentAuthoringState |
property | 返回隔离的当前 schema 4 Authoring State;非 Authoring 文档返回 null。 |
workspace.preparePersistence() |
method | 从当前 working document 准备唯一 canary candidate。 |
workspace.acceptPersistedSource() |
method | 精确验证成功响应并推进 source。 |
workspace.rejectPersistence() |
method | 记录 transport conflict/error,同时保留 working 和 History。 |
RenderReportRuntimeSurface |
class | 只读运行预览和正式报表表面。 |
RenderReportView |
class | 基于 ReportVirtualModel 的通用虚拟报表视图。 |
compileReportTemplate() |
function | 校验并编译 Cell/Relation 模板。 |
materializeReportTemplate() |
function | 把模板和数据集物化为运行文档。 |
readReportTemplateDocumentJson() |
function | 严格读取 schema 3 或 schema 4 文档,不隐式迁移。 |
compileReportTemplateDocument() |
function | 编译版本中立的 Report 文档。 |
materializeReportTemplateDocument() |
function | 按 feature policy 物化版本中立文档。 |
createEmptyReportRuntimeViewState() |
function | 为一个报表文档创建不持久化的运行视图状态。 |
reduceReportRuntimeViewState() |
function | 以 immutable intent 更新 Group/Axis 筛选、排序和折叠状态。 |
reconcileReportRuntimeViewState() |
function | Dataset 或模板刷新后丢弃 stale node/target,并保留可诊断的 stale member。 |
searchReportRuntimeMemberCandidates() |
function | 在完整本地 Candidate Catalog 上搜索,每次最多返回 200 个 UI 候选。 |
migrateReportTemplate() |
function | 显式生成 schema 3→4 迁移审计结果,不保存结果。 |
createBlankReportPlacementTemplate() |
function | 直接创建不含假 Cell、ReportGraph 或 Placement 的 canonical schema 4 空模板。 |
createReportDocumentAuthoringSession() |
function | 创建隔离 source/working/history 的 schema 4 canary Authoring Session。 |
prepareReportDocumentPersistenceCandidate() |
function | 对 canonical schema 4 working document 执行严格 Writer pipeline。 |
verifyPersistedReportDocumentResponse() |
function | 在推进 Session source 前严格验证持久化响应。 |
applyReportBorderCommand() |
function | 在已存在的内容 Cell 或显式布局 Cell 上应用 Cell Border。 |
prepareReportTemplateForPersistence() |
function | schema 3 默认 writer:执行保存前压缩、校验和 JSON 输出。 |
REPORT_TEMPLATE_SCHEMA_VERSION |
constant | 当前默认可写持久化模板 schema 版本,仍为 3。 |
REPORT_PLACEMENT_TEMPLATE_SCHEMA_VERSION |
constant | Placement 文档 schema 版本,为 4;不负责开启 canary writer。 |
REPORT_EXPORT_SCHEMA_VERSION |
constant | 当前快照和工作簿导出 schema 版本。 |
copyVirtualReportRangeToTsv() |
function | 复制指定报表区域。 |
exportVirtualReportToTsv() |
function | 导出整个虚拟报表。 |
所有 API 均从 ds-ui 包入口导入;设计器内部状态和旧版 Region-first 对象不属于公共 API。
版本中立的读取和运行类型合同包括 ReportTemplateDocument、
ReportTemplateDocumentReadResult、ReportTemplateDocumentDiagnostic、
ReportTemplateDocumentMaterializeContext、CompiledReportTemplateDocument、
ReportRuntimeProjection、ReportDocumentFeaturePolicy、
ReportRuntimeViewState、ReportRuntimeViewIntent、ReportRuntimeInteractionIndex、
ReportAxisProjectionEntry、ReportPlacementAxisProjection、
ReportPlacementTemplate、ReportPlacement、ReportFrameStyle、
ReportDocumentBorderDecoration、ReportTemplateMigrationResult、
ReportTemplateMigrationDiagnostic、ReportWorkspaceDocumentState 和
ReportWorkspacePlacementFieldResult。
schema 4 canary Authoring/Writer 合同包括 ReportDocumentAuthoringSession、
ReportDocumentAuthoringState、ReportDocumentEditBatch、
ReportDocumentEditCommand、ReportDocumentEditResult、
ReportDocumentPersistenceRequest、ReportDocumentPersistenceCandidate、
PersistedReportDocumentResponse、PrepareReportDocumentSessionPersistenceResult、
AcceptReportDocumentPersistenceResult 和 RejectReportDocumentPersistenceResult。
当 Workspace 在 schema 4 canary 中执行 placeField() 时,返回
ReportWorkspacePlacementFieldResult;该结果只暴露 placement status、selection、diagnostics
和不可变 Authoring State,不暴露内部 schema 3 semantic 临时模板。schema 3 Workspace
仍返回原有 ReportFieldPlacementResult。
自定义 Authoring UI 可使用 createReportDocumentAuthoringSurfaceModel() 和
createReportDocumentAuthoringIntent();对应 ReportDocumentAuthoringSurfaceModel、
ReportDocumentAuthoringSelection 与 ReportDocumentAuthoringIntent 会把 action reason、
session identity 和 working revision 固定在同一份模型中,宿主必须同时校验 identity 与 revision
后再执行 intent。
快速开始
import { REPORT_TEMPLATE_SCHEMA_VERSION, RenderReportWorkspace, type ReportDatasetMap, type ReportTemplate,} from 'ds-ui' const template: ReportTemplate = { schemaVersion: REPORT_TEMPLATE_SCHEMA_VERSION, rows: [{ height: 30 }, { height: 30 }], columns: [{ width: 140 }, { width: 100 }], cells: [ { id: 'item', address: 'A2', row: 1, column: 0, value: { type: 'field', dataset: 'orders', field: 'item' }, binding: { dataset: 'orders', mode: 'group', groupBy: ['item'] }, expansion: { direction: 'down' }, }, { id: 'amount', address: 'B2', row: 1, column: 1, value: { type: 'field', dataset: 'orders', field: 'amount' }, binding: { dataset: 'orders', mode: 'value', field: 'amount' }, }, ], relations: [ { type: 'structure', from: 'item', to: 'amount', kind: 'child' }, { type: 'layout', from: 'item', to: 'amount', kind: 'rightOf' }, { type: 'scope', from: 'item', to: 'amount', kind: 'inherit' }, ], datasets: [{ id: 'orders', fields: [ { id: 'item', type: 'text', role: 'dimension' }, { id: 'amount', type: 'number', role: 'measure' }, ], }],} const datasets: ReportDatasetMap = { orders: [ { item: '门诊', amount: 120 }, { item: '住院', amount: 260 }, ],} const workspace = new RenderReportWorkspace({ template, datasets, onTemplateChange: next => { void next }, onTemplateExport: json => { void json },})
Schema 4 Placement canary Authoring 与 Writer
schema 4 已提供严格 reader、只读 Runtime、隔离 Authoring Session 和纯 Framework Writer。
这些写能力仅用于本地开发、自动化测试和受控 canary;默认产品写入路径仍是 schema 3,
REPORT_TEMPLATE_SCHEMA_VERSION 仍为 3。REPORT_PLACEMENT_TEMPLATE_SCHEMA_VERSION 只标识
Placement 协议 4,不得用任一常量覆盖已读取文档的 schemaVersion。
Feature policy 默认关闭;两个开关都必须由受控宿主显式启用:
placementReadMode默认为reject;显式设为readOnly才物化 schema 4 Preview;placementWriteMode默认为disabled;显式设为canary才创建 schema 4 Authoring Session;- 未知 policy 值按关闭处理;仅把 Workspace 切换到
design不会开启 writer。
import { compileReportTemplateDocument, materializeReportTemplateDocument, readReportTemplateDocumentJson,} from 'ds-ui' declare const storedJson: string const read = readReportTemplateDocumentJson(storedJson)if (!read.document) { throw new Error(read.diagnostics.map(item => item.message).join('; '))} const compiled = compileReportTemplateDocument(read.document) // schema 4 Runtime 默认被 policy 拒绝。只有显式 readOnly 才会生成预览投影。const projection = materializeReportTemplateDocument({ compiled, featurePolicy: { placementReadMode: 'readOnly' }, datasets: {},}) if (projection.kind === 'placement') { console.assert(projection.resizeMode === 'disabled')}
Flag-off 只读 Preview
只读 Workspace 使用原始 JSON,并只开启 read policy:
import { RenderReportWorkspace } from 'ds-ui' declare const storedJson: string const workspace = new RenderReportWorkspace({ templateDocumentJson: storedJson, featurePolicy: { placementReadMode: 'readOnly' }, mode: 'preview',}) const readOnlyState = workspace.documentStateconsole.assert(readOnlyState.kind === 'placementReadOnly')console.assert(readOnlyState.capabilities.canEdit === false)console.assert(readOnlyState.capabilities.canExport === false)console.assert(readOnlyState.capabilities.canPersist === false)console.assert(workspace.template === null)
Source、working 与 Preview 分离
Authoring Session 保留三类彼此隔离的状态:
sourceJson是数据库返回的精确原文,只在严格验证后的 reload/save success 后推进;workingDocument是独立深拷贝,成功 batch、Undo 和 Redo 只修改 working;- Working Preview 从当前 working revision 加运行时 Dataset/Parameter overlay 物化,overlay 不回写 source、working 或 Writer candidate。
dirty working document 切换 source 时,Workspace 默认返回 false;调用方必须明确传入
{ discardChanges: true }。即使允许 discard,非法或 blocking replacement 也不会替换当前 Session。
Preview 中 Placement/Decoration 编辑按钮保持只读;Undo、Redo、Discard 仍可按 Session history 使用。
import { RenderReportWorkspace } from 'ds-ui' declare const storedJson: string const authoringWorkspace = new RenderReportWorkspace({ templateDocumentJson: storedJson, featurePolicy: { placementReadMode: 'readOnly', placementWriteMode: 'canary', }, mode: 'design',}) console.assert(authoringWorkspace.documentState.kind === 'placementAuthoring')console.assert(authoringWorkspace.documentState.capabilities.canPersist === true)console.assert(authoringWorkspace.documentAuthoringState?.mode === 'placementAuthoring') // canPersist 只表示 Framework 能准备 canary candidate;// 它不是 Backend writer flag、业务权限或生产发布授权。 // 未显式 discard 时,dirty working document 不会被新 source 覆盖。const nextStoredJson = storedJsonconst switched = authoringWorkspace.loadTemplateDocumentJson(nextStoredJson)if (!switched) { authoringWorkspace.loadTemplateDocumentJson(nextStoredJson, authoringWorkspace.datasets, { discardChanges: true, })}
Create、Edit、Undo、Prepare 与 Accept
Authoring Session 不调用网络。业务宿主持有 candidate,并把响应交回 Session 做精确验证:
import { createBlankReportPlacementTemplate, createReportDocumentAuthoringSession, type PersistedReportDocumentResponse, type ReportDocumentPersistenceCandidate,} from 'ds-ui' const blank = createBlankReportPlacementTemplate()blank.datasets = [{ id: 'orders', fields: [{ id: 'name', type: 'text' }],}] const created = createReportDocumentAuthoringSession({ sourceJson: JSON.stringify(blank), policy: { placementWriteMode: 'canary' },})if (created.status !== 'accepted') throw new Error(created.failureReason.message)const session = created.session const edit = session.applyBatch({ commands: [ { domain: 'cell', command: { type: 'createCell', cell: { id: 'name', row: 0, column: 0, binding: { dataset: 'orders', mode: 'value', field: 'name' }, }, }, }, { domain: 'placement', command: { type: 'createPlacement', anchorCellId: 'name' }, }, ],})if (edit.status === 'rejected') throw new Error(edit.failureReason.message)if (edit.status !== 'applied') throw new Error('Edit produced no canonical change') session.undo()session.redo() const prepared = session.preparePersistence({ resourceId: 'report-1', writeIntent: 'CREATE_V4', expectedVersionNo: 0,})if (prepared.status !== 'prepared') throw new Error(prepared.failureReason.message) declare const saveCanaryTemplate: ( candidate: ReportDocumentPersistenceCandidate,) => Promise<PersistedReportDocumentResponse> const response = await saveCanaryTemplate(prepared.candidate)const accepted = session.acceptPersistedSource(prepared.candidate, response)if (accepted.status === 'persistenceIntegrityConflict') { // response 不一致时 working/history 保留,并要求重新加载远端 source。 console.assert(accepted.requiresReload === true)} else if (accepted.status === 'rejected') { throw new Error(accepted.failureReason.message)}
使用 Workspace 时可通过同名 wrapper 完成相同流程,而不需要取得内部 Session:
import type { PersistedReportDocumentResponse, RenderReportWorkspace, ReportDocumentPersistenceCandidate,} from 'ds-ui' declare const authoringWorkspace: RenderReportWorkspacedeclare const saveWorkspaceCandidate: ( candidate: ReportDocumentPersistenceCandidate,) => Promise<PersistedReportDocumentResponse> const preparedByWorkspace = authoringWorkspace.preparePersistence({ resourceId: 'report-1', writeIntent: 'UPDATE_V4', expectedVersionNo: 7,})if (preparedByWorkspace?.status === 'prepared') { const response = await saveWorkspaceCandidate(preparedByWorkspace.candidate) const accepted = authoringWorkspace.acceptPersistedSource( preparedByWorkspace.candidate, response, ) if (accepted?.status === 'persistenceIntegrityConflict') { console.assert(accepted.requiresReload) }} // 网络、权限或并发失败时由宿主报告 transport outcome。// authoringWorkspace.rejectPersistence(candidate, 'conflict' | 'error')
纯 Writer API 也可由不使用 Session 的受控宿主调用:
import { prepareReportDocumentPersistenceCandidate, verifyPersistedReportDocumentResponse, type PersistedReportDocumentResponse, type ReportTemplateDocument,} from 'ds-ui' declare const sourceCanonicalJson: stringdeclare const workingDocument: ReportTemplateDocumentdeclare const persistedResponse: PersistedReportDocumentResponse const candidateResult = prepareReportDocumentPersistenceCandidate({ resourceId: 'report-1', writeIntent: 'UPDATE_V4', expectedVersionNo: 7, sourceCanonicalJson, workingDocument,})if (candidateResult.status === 'prepared') { const verification = verifyPersistedReportDocumentResponse( candidateResult.candidate, persistedResponse, ) if (verification.status !== 'verified') console.assert(verification.requiresReload)}
持久化响应只有同时满足以下条件才会推进 source:resource id 与 candidate 一致、
schemaVersion === 4、versionNo === expectedVersionNo + 1、响应模板通过严格 Reader/full Compiler/
stable round-trip,并且 canonical JSON 与 candidate target 完全相同。网络错误、409、403、500 或
response integrity conflict 不会清空 working、dirty 或 History。
schema 4 仍不得传给 schema 3 的 prepareReportTemplateForPersistence();任何路径都不得将
schema 4 降级为 schema 3,也不得通过类型断言绕过版本边界。
migrateReportTemplate() 只返回 schema 3→4 迁移审计结果,不自行调用网络。宿主可以先对全部模板
dry-run,并在 reviewRequired=false、无 error 且 schema 4 strict round-trip 通过后,通过自己的
条件写入接口批量保存。当前 canary Writer 不接受 reviewRequired、hybrid 输入或隐式 load-time
migration;遇到歧义必须停止,不能猜测边框归属。
Canary 限制
以下能力明确延期,不能把当前 API 描述为完整生产写入方案:
reviewRequiredmigration 的通用人工裁决 UI;无歧义模板可由宿主自动迁移;- Backend authoritative semantic validator、append-only revision audit 和权限强化;
- Dataset/Parameter catalog reconciliation 与 catalog mutation revision;
- many-to-many topology reconciliation、完整 ReportGraph duplicate、schema 4 Clipboard/Paste/Duplicate;
- 自动保存、多人协作、生产级全量 rollout;
- 面向不受信任第三方的通用 schema 4 写接口。
模板模型
ReportTemplate 由 Cell 和 Relation 组成:
- Cell 保存地址、值、数据绑定、展开方向、格式和样式;
structurerelation 表达父子语义;layoutrelation 表达below、rightOf或对齐关系;scoperelation 表达继承、独立、连接、聚合或普通 Cell 运行上下文;axis与crossrelation 表达交叉报表的行轴、列轴和指标;- dataset 与 parameter 定义属于模板资源,但运行数据通过
ReportDatasetMap传入。
Dataset 字段的 type 与 role 是两个独立维度。type 描述实际值类型并约束聚合函数;role 只是设计期的维度/度量推荐,不限制字段用途。文本、日期、布尔和数字字段都可以用于 Group、Row Axis 或 Column Axis,因此年份、季度、数字编码即使标记为 role: 'measure' 也仍可直接分组。COUNT 接受任意字段;当前 SUM、AVG、MIN 和 MAX 只接受 type: 'number'。
使用 parseReportTemplateJson() 读取外部 JSON。新建模板时使用 REPORT_TEMPLATE_SCHEMA_VERSION,消费导出快照或工作簿结构时使用 REPORT_EXPORT_SCHEMA_VERSION;两者是独立协议版本。持久化前调用 prepareReportTemplateForPersistence();它会压缩设计模板、执行 JSON round-trip 和 compiler 校验,仅在没有 error 时返回可保存的 JSON。不要通过 JSON.stringify() 或类型断言绕过保存门禁。
Cell value 只支持 text、field、aggregate 和 expression。小计与总计由 aggregate binding 配合 layout/scope relation 表达,不是独立 value 类型。Cell 与 Dataset 字段格式统一使用 ReportValueFormat typed formatter,例如:
聚合 relation 使用显式父格上下文,不再使用 aggregateScope 或 scopeRefs:
const aggregateCells = [{ id: 'reportTotal', row: 3, column: 2, binding: { dataset: 'main', mode: 'aggregate', aggregate: { func: 'sum', field: 'amount' } },}] const aggregateRelations = [ // 每个国家实例聚合一次,Dataset 继承自国家格 { type: 'scope', to: 'countryTotal', kind: 'aggregate', rowParentCellId: 'country' }, // 国家与月份运行实例的交集 { type: 'scope', to: 'countryMonthAmount', kind: 'aggregate', rowParentCellId: 'country', columnParentCellId: 'month', }, // 无父格时 Dataset 显式保存在 Aggregate Cell binding 上 { type: 'scope', to: 'reportTotal', kind: 'aggregate' },]
存在 Row 或 Column Parent 时 Cell Dataset 必须为空并从父格继承;无父格时 Cell 必须显式声明 Dataset。两个父格必须解析到同一 Dataset。Aggregate scope relation 本身不保存 Dataset。旧的 aggregateScope/scopeRefs 模板属于已移除协议,JSON 读取会直接报错。
设计端的 Aggregate Cell 直接配置 Row Context Parent Cell 和 Column Context Parent Cell,不再提供 Aggregate Scope 枚举。有任一父格时,Dataset 显示为只读的 Inherited: <dataset>;移除最后一个父格会清空 Dataset,但保留 Field、Aggregate Function 和格式,用户随后必须显式选择 Dataset 才能通过保存校验。
Parent 决定数据上下文和运行实例数;Row Parent Alignment 与 Column Parent Alignment 分别决定 Cell 如何映射到父格展开后的运行区间。两个方向互不影响,默认均为 End:
Start:保持相对父格起点的设计偏移,Span 不变。End:保持相对父格终点的设计偏移,Span 不变。Stretch:起点跟随父格起点、终点跟随父格终点,动态扩展rowSpan或columnSpan。只有 Cell 与对应父格的设计投影重叠时才合法。
例如年份父格在设计态占 C 列,运行时因季度展开为 C~F:同样位于 C 列的上下文 Cell 使用 Start 时落在 C,使用默认 End 时落在 F,使用 Stretch 时合并覆盖 C~F;设计在 D 列并使用 End 的年度聚合 Cell 会落在 G。父格本身采用 Merge 或 Repeat 只影响显示,不改变上述区间映射。
Summary 的逻辑实例唯一由 Cell + Row Parent实例 + Column Parent实例 确定。Row/Column Parent 同时存在时取两边实例的笛卡尔积,数据作用域为两个父上下文数据行的交集。小计行、小计列或它们的交点只是布局结果,不会额外生成第二个逻辑实例。父格运行内容区间包含其明细、下级分组和下级小计,但不包含当前父级自己的边界槽,因此默认 End 不会循环包含自身或多偏移一格。Cell 同时位于行、列父格的末端时,会自然落在小计行与小计列的交点。
rowParentCellId: 'country'+columnParentCellId: 'year'表示每个国家、每个年度一个小计;将 Cell 设计在季度交叉值右侧,会在每个年度的季度展开后出现。rowParentCellId: 'country'+columnParentCellId: undefined表示每个国家跨全部年度的总计;将 Cell 设计在整个交叉区域右侧,会被横向展开自然推到行尾,不会生成额外小计行。rowParentCellId: undefined+columnParentCellId: 'year'表示每个年度跨全部国家的总计;将 Cell 设计在交叉区域下方,会被纵向展开自然推到列底。- Row/Column Parent 都为空时表示整个 Dataset 的总计,Dataset 必须由 Aggregate Cell 显式声明。它同样保留设计网格中的行列、间距和合并关系;多个总计 Cell 设计在同一行时,运行时仍保持同行,不会改成对角线排列。
- 需要把上下文 Cell 保留在父格首端时显式选择
Start;需要覆盖整个父格运行跨度时选择Stretch。真正需要每个下级实例都生成一次时,应改为绑定下级 Parent,而不是使用 Stretch。 - 同一父上下文下的 Cell 只有在设计网格同一行时才会组成同一个 Summary Fragment;设计在不同行的小计会保持独立行并按原顺序重复。
这些都是普通 Aggregate Cell,不需要额外的“行尾总计”、“列底总计”或 axisEnd 类型。
普通静态文本 Cell 也可以通过 scope.context 使用相同的 Row/Column Parent 运行上下文,而不绑定任何 Dataset 字段。它的出现次数由父格实例决定,位置继续受行列扩展推动,文本、样式和合并 Span 原样保留。这样可让合并的“小计”标签与相邻聚合格共同重复:
const subtotalCells = [ { id: 'subtotalLabel', row: 2, column: 0, columnSpan: 2, value: { type: 'text', text: '小计' } }, { id: 'subtotalAmount', row: 2, column: 2, binding: { mode: 'aggregate', aggregate: { func: 'sum', field: 'amount' } } },] const subtotalRelations = [ { type: 'scope', to: 'subtotalLabel', kind: 'context', rowParentCellId: 'country' }, { type: 'scope', to: 'subtotalAmount', kind: 'aggregate', rowParentCellId: 'country' },]
属性面板对普通文本和 Aggregate Cell 均显示 Row Context Parent Cell 与 Column Context Parent Cell;设置父格后还会显示对应的 Row Parent Alignment 或 Column Parent Alignment。context 只控制运行实例、继承上下文和父格区间映射,不会把静态文本改成字段或聚合绑定。
const moneyFormat = { formatter: { type: 'currency' as const, currency: 'CNY', minimumFractionDigits: 2, maximumFractionDigits: 2, },}
保存示例:
import { prepareReportTemplateForPersistence, type ReportTemplate,} from 'ds-ui' declare const template: ReportTemplatedeclare const saveTemplate: (json: string) => Promise<void> const persistence = prepareReportTemplateForPersistence(template)if (!persistence.valid || !persistence.json) { throw new Error(persistence.diagnostics.map(item => item.message).join('; '))}await saveTemplate(persistence.json)
编译与物化
import { compileReportTemplate, materializeReportTemplate, type ReportTemplate,} from 'ds-ui' const template: ReportTemplate = { schemaVersion: 3, rows: [{}], columns: [{}], cells: [], relations: [],}const compiled = compileReportTemplate(template)const document = materializeReportTemplate({ compiled, datasets: {} })
compileReportTemplate() 生成 Cell/Relation 图结构与诊断;materializeReportTemplate() 生成运行文档和 ReportVirtualModel。业务侧需要自定义工作台时,可以组合这些无 UI API,而不必复制 Workspace 内部状态。
RenderReportWorkspace
RenderReportWorkspace 提供设计、预览、数据集字段、属性、数据预览和校验区域。Preview 中 Cell 属性面板保持可见但完全只读,字段放置、Cell 内容与语义、关系、合并、冻结、粘贴和 Duplicate 等模板修改入口只在 Designer 生效。常用选项包括:
template、datasets、parameterValues和root;templateDocumentJson和featurePolicy,用于 schema 3/4 双读、schema 4 read-only 或受控 canary Authoring;runtimeRevision,用于拒绝晚到的旧异步结果,并在数据对象引用未变化时显式使 Preview 缓存失效;locale、timeZone和valueFormatterRegistry,用于统一 Workspace Preview 的值格式上下文;mode: 'design' | 'preview';previewDataState和previewStateMessage,用于在业务数据加载期间显示 Preview Loading 或 Error;showToolbar、showStatusBar、showResourceTools;onTemplateChange、onTemplateExport、onModeChange;onGridStructureActionsChange,用于将行列插入、删除命令接入业务工具栏;onFreezeActionsChange,用于将冻结行列命令接入业务工具栏;onDrillthrough,用于接收 Preview 中“查看明细”命令的完整运行时上下文;onGridInsertActionsChange保留为只包含插入动作的兼容回调。
schema 4 canary 下,Workspace 的 applyDocumentEditBatch()、undoDocumentEdit()、
redoDocumentEdit()、discardDocumentEdits() 以及 Placement、Frame、Layout Cell、Document Decoration
便捷方法都只修改 Session working document。reconcileReportSplit() 与 reconcileReportMerge() 要求显式
Placement identity reconciliation。Workspace 本阶段仍不提供 schema 4 保存网络调用;业务宿主通过
Authoring Session 的 preparePersistence() 和 acceptPersistedSource() 完成受控保存。
Workspace 内置 Toolbar 在工作区生命周期内保持同一个 RenderToolbar 实例,模板刷新、模式切换和命令状态更新只同步 Toolbar groups,不会反复注册焦点对象。showToolbar: false 时不会创建隐藏的 Toolbar,也不会向焦点域注册不可见命令入口。
业务端同时更新 Dataset、查询参数和 Root Context 时,应使用一次 setRuntimeContext() 批量提交,避免连续 setter 触发多次 Preview 物化:
import type { RenderReportWorkspace, ReportDatasetMap, ReportParameterValues,} from 'ds-ui' declare const workspace: RenderReportWorkspacedeclare const loadDatasets: () => Promise<ReportDatasetMap>declare const parameterValues: ReportParameterValuesdeclare const root: Record<string, unknown> const requestRevision = 12const loadingAccepted = workspace.setRuntimeContext({ runtimeRevision: requestRevision, dataState: 'loading', stateMessage: '正在加载预览数据…',}) const datasets = await loadDatasets()const resultAccepted = workspace.setRuntimeContext({ runtimeRevision: requestRevision, datasets, parameterValues, root, dataState: 'ready', stateMessage: '',}) if (!loadingAccepted || !resultAccepted) { // 更高 revision 的请求已开始,本次结果不再更新业务状态。}
setRuntimeContext() 返回本次更新是否被接受。每个异步请求开始时递增一次 runtimeRevision,该请求的 Loading、成功和失败提交都使用同一个值;低于 Workspace 当前 revision 的提交返回 false,且不会修改 Dataset、Root Context、Preview 状态或消息。省略 revision 时保持兼容行为,但业务端需要自行处理请求竞态。
更高的 runtimeRevision 会使已物化 Preview 失效,即使 Dataset 仍是同一个对象引用;相同 revision 且运行上下文没有变化时继续复用 Preview。从 Designer 再次进入 Preview 不会重复计算。是否重新请求 SQL/API 数据由业务端负责,设计预览通常应缓存已加载 Dataset,只在用户明确刷新时重新请求。
Workspace 的格式选择顺序是:Cell 的内联 format、Cell 的 formatRef、根据值和字段类型推断的默认格式。选定格式后,format.locale 优先于 Workspace locale,日期 formatter 自己的 timeZone 优先于 Workspace timeZone;自定义 formatter 从 valueFormatterRegistry 查找。修改 registry 的注册内容会改变它的 revision;随后调用 setRuntimeContext({ valueFormatterRegistry: registry }) 即可重新物化 Preview,不需要替换 registry 对象。
gridStructureActions 根据当前单元格或矩形选区生成插入、删除动作;applyGridStructureAction() 执行动作。删除矩形选区时会一次删除覆盖的行或列,并把选区重定位到最近的有效网格位置。Ctrl/Cmd 形成的非连续 Cell 多选不会被隐式扩大为外接矩形;存在行或列空档时,对应的批量删除动作会禁用。报表始终至少保留一行和一列。在合并 Cell 的 Span 内部插入行列时会扩展对应 Span,确保原合并区域的结构范围不被截断。旧的 gridInsertActions、applyGridInsertAction() 和四个插入便捷方法继续可用。
mergeActions、applyMergeAction()、mergeSelection() 和 unmergeSelection() 提供矩形合并操作。合并采用数据安全规则:选区必须是至少两个格子的连续矩形,不能越界或切穿已有合并单元格。多个普通静态文本 Cell 可以合并,文本按从上到下、每行从左到右的顺序直接拼接,空白 Cell 会被忽略,合并结果使用左上锚点的样式。选区只有一个数据绑定、表达式、扩展或关系 Cell 且其余位置为空时也允许合并,并保留该语义 Cell 的 id、内容、格式、样式和关系。多个语义 Cell,或语义 Cell 与非空静态文本混合时,动作会禁用并返回诊断,不会覆盖数据。
取消合并只移除锚点的 rowSpan 和 columnSpan,保留锚点内容与关系,其余位置恢复为空网格槽。静态文本合并后,取消合并会把拼接结果保留在左上角,不会自动拆回原来的多个文本。onMergeActionsChange 可用于同步业务工具栏的启用状态和冲突提示。
Designer 的行号和列标支持单击或拖动选择整行、整列以及连续多行、多列。freezeActions 和 applyFreezeAction() 将表头选区转换为模板 freeze 设置:冻结行必须选择从第 1 行开始的 Top N 连续整行,冻结列必须选择从 A 列开始的 Left N 连续整列;只选择中间行列时动作会禁用并返回边界原因。冻结行、冻结列可以分别取消,也可以一次取消全部冻结。freeze.rightColumns 仍由模板 API 支持,当前 Toolbar 只编辑顶部行和左侧列。
冻结属于数据查看行为。Designer 只编辑并持久化 template.freeze,设计网格自身始终按未冻结状态滚动,但会在冻结行列边界绘制深色指示线;指示线随普通网格内容滚动,只用于帮助设计人员感知冻结范围。设计预览、运行预览和正式 Runtime 才执行真实冻结。Preview 中所有新增、取消冻结动作均为只读禁用状态。左右冻结列的总数不能超过模板列数。运行视图会在冻结区域边界持续绘制深色分隔线和渐变阴影,滚动内容进入冻结区域下方或右侧时阴影会增强。行列结构命令会同步维护冻结边界:在 Top N 或 Left N 冻结区域内部插入会扩大冻结数量,在边界及普通区域插入不会改变冻结数量;删除冻结区域内的行列会按重叠数量缩小冻结范围。右侧冻结列也遵循相同的边界维护规则。
设计器通过 cellClipboardActions、copySelectedCells()、pasteCopiedCells()、duplicateSelectedCells() 和 applyCellClipboardAction() 提供结构化 Cell 复制。内部剪贴板保存源矩形、Cell 相对坐标、内容、绑定、格式、样式、条件和 Span;粘贴时生成新 Cell id,并重映射选区内部关系和 group order 的 Cell 引用。指向选区外部的关系不会复制。复制多个 Cell 时必须使用连续矩形选区;Ctrl/Cmd 形成的不规则离散多选不会按外接矩形隐式带入未选 Cell,Copy 和 Duplicate 会直接禁用并返回原因。动作状态使用只读检查,只有真正复制时才构造剪贴板;Duplicate 使用按模板缓存的占用区间,按钮状态会准确反映是否存在空目标区域。
粘贴目标取当前 Cell、矩形选区左上角或空网格槽。第一版采用非覆盖策略:越界、切穿合并 Cell 或目标存在有效 Cell 时禁用粘贴。Duplicate 优先尝试选区右侧,其次下方,再查找首个连续空区域。Designer 支持 Ctrl/Cmd+C、Ctrl/Cmd+V、Ctrl/Cmd+D;这些快捷键操作模板内部剪贴板,Preview 的 TSV 复制行为不变。Backspace 清空当前 Cell 或矩形选区中的所有 Cell 内容;批量清空按语义依赖从子 Cell 到父 Cell 执行,任意一个 Cell 仍被选区外语义关系引用时整批拒绝,不会只清除一部分。onCellClipboardActionsChange 可用于同步业务工具栏。
单元格条件样式在 Cell PropertyGrid 的 Conditional Style / Rules 属性中统一管理。该属性显示规则数量,激活后打开结构化编辑弹窗,可新增、删除和逐条编辑数据来源、比较运算符、比较值以及匹配后的文字和颜色样式;编辑期间只修改草稿,点击 Apply 后才一次写回 Cell 的 conditions。
固定行高文本排版
Report Cell 默认继续使用单行省略号。需要多行或固定区域缩放时,在 ReportCellStyle 中设置:
const style = { wrap: true, textOverflow: 'ellipsis' as const, textFit: 'shrink' as const,}
wrap:识别显式换行并按 Cell 可用宽度自动换行。textOverflow: 'clip' | 'ellipsis':固定行高放不下时裁剪或在最后可见行显示省略号。textFit: 'none' | 'shrink':在内部可读字号下限内缩小文字;到达下限后再执行溢出策略。- 行距来自当前主题字体度量,不提供单独的 Line Height 或最小字号配置。
- Preview 和 Runtime 绘制不会根据文字内容动态修改行高,因此虚拟滚动总高度和冻结区保持稳定。
Designer 的 Auto Fit Row Height 是显式命令:它按当前 Preview 样本或设计占位内容计算建议高度,
然后通过现有 row resize 命令写回固定 template.rows[row].height。第一版一次只处理一个模板行,
忽略 rowSpan > 1 的纵向合并 Cell。业务宿主隐藏内置 Toolbar 时,可通过
workspace.rowAutoFitActions、workspace.applyRowAutoFitAction() 和
onRowAutoFitActionsChange 接入自己的工具栏。
单元格边框
ReportCellStyle.border 按 top、right、bottom、left 四边保存边框,每条边可独立设置 style、color 和 width。线型支持 none、solid、dashed、dotted。屏幕上未声明的边使用当前主题的 borderData;Workbook 导出会使用独立的纸张网格默认值。style: 'none' 表示显式关闭该边。旧模板中的 borderColor 和 borderWidth 仍可读取,但新的设计操作只写四边模型。
const style = { border: { top: { style: 'solid', width: 2, color: { r: 60, g: 70, b: 80, a: 1 } }, bottom: { style: 'dashed', width: 1, color: { r: 120, g: 130, b: 140, a: 1 } }, left: { style: 'none' }, },}
Workspace Toolbar 的 Borders 下拉菜单和 PropertyGrid 的 Style / Borders 属性都支持 Top、Bottom、Left、Right、All、Outer、Inner、Inner Horizontal、Inner Vertical 和 No Borders。当前这些命令只编辑 Cell Border:每个被预设影响的网格槽都必须已经存在 Content Cell 或显式 Layout Cell。Content Cell 包含文本、数据绑定、表达式或其他持久化内容;显式 Layout Cell 可以没有值,但它由 structure、scope 等 Relation 引用,因而是报表结构的一部分。Designer 为显示网格而补齐的占位 Cell,以及模板中完全未使用的空白槽,都不是显式 Layout Cell。
如果 Top、Outer、All 等预设需覆盖的任一槽位是空白槽或旧版边框装饰 Cell,Workspace Toolbar 会禁用该预设并显示首个不支持的行列位置,PropertyGrid 的 Borders、Border Style、Border Color 和 Border Width 行也会变为只读,不会自动生成 cell_border_* Cell。schema 3 下一次 Cell Border 操作还只能覆盖同一个 ReportGraph;选区包含两个或更多独立 ReportGraph 时会被拒绝,应拆分选区并分别设置各报表的边框。直接调用 applyReportBorderCommand() 也使用同一检查:拒绝时 changed 为 false、原模板保持不变,原因通过 diagnostic 返回。因此,对稀疏区域设置边框前,需先在模板中建立并用 Relation 保留所需的空布局 Cell;否则应只选择已存在的 Cell。选区切穿已有合并 Cell 时同样会拒绝,必须完整选择该合并区域。
decoration: 'border' 只是 schema 3 旧模板的兼容输入,新的边框命令不再创建这类 Cell。No Borders 仍可清理它们:无 Relation 引用的纯边框装饰 Cell 会被删除;被 Relation 引用的旧装饰 Cell 会移除装饰标记和边框样式,但保留为空布局 Cell,Relation 也会保留。旧模板只要包含至少一个 ReportNode 且仍包含边框装饰 Cell,Compiler 就会为每个该类 Cell 生成 ambiguousLegacyBorderDecoration warning,提醒业务方人工确认它是固定文档边框还是本应跟随某个报表的边框;完全不含 ReportNode 的纯 Document 模板不生成该 warning。
运行时先绘制所有 Cell 的背景和内容,再跨滚动区、冻结行和冻结列统一绘制边框,因此粗边框和虚线不会被相邻 Cell 背景覆盖。相邻 Cell 的共享边按最小网格边段只绘制一次;合并 Cell 的长边会与相邻普通 Cell 的各段分别决议。显式边框优先于继承边框,显式 none 可关闭共享边,其他冲突按宽度和线型稳定决议。PropertyGrid 修改 Border Color 或 Border Width 时只修改对应属性,不会抹平各边已有的线型;宽度设为 0 后再恢复正数仍使用原线型。后续 Toolbar 边框预设使用最近一次有效的边框线型、颜色和宽度。条件样式按边深合并:只覆盖 bottom 不会丢失另外三边。为兼容旧模板,导出快照和工作簿中间模型仍会在 borderDecorations 中保留已物化旧装饰的运行时坐标及四边样式,并深复制边框颜色与线型配置。
使用 REPORT_WORKSPACE_ITEM_IDS 和 ReportWorkspaceItemId 引用稳定工作区 id。关系图和 JSON 诊断默认属于 debug 能力;普通业务页面应以 Cell、字段、属性和预览为主要入口。
运行预览
import { RenderReportRuntimeSurface, compileReportTemplate, materializeReportTemplate, type ReportTemplate,} from 'ds-ui' const template: ReportTemplate = { schemaVersion: 3, rows: [{}], columns: [{}], cells: [], relations: [],}const compiled = compileReportTemplate(template)const document = materializeReportTemplate({ compiled, documentKey: 'orders-report:v1', datasets: {}, locale: 'zh-CN', timeZone: 'Asia/Shanghai',})const preview = new RenderReportRuntimeSurface({ document, showGridHeaders: true, minHeight: 320, onRowHeightChange: (row, height) => { void row; void height }, onColumnWidthChange: (column, width) => { void column; void width },})
运行面只接收已经完成物化的 Report document。runtimeRevision、Dataset、locale、timeZone 和 formatter registry 属于 Workspace 或 materializer 的输入,不由 RenderReportRuntimeSurface 管理。直接使用 RuntimeSurface 时,格式上下文变化后应重新调用 materializeReportTemplate(),再把新 document 赋给运行面。启用网格头后,用户可以调整来源模板行高和列宽;重复展开的运行行列会映射回对应模板行列。
Group/Axis 运行视图控制
RuntimeViewState 是宿主持有的会话状态,不写入 schema 3/schema 4 Template、Writer 或 Designer History。运行顺序固定为“筛选 graph-local rows → 分组/聚合 → 排序 → 折叠槽位 → Projection”;不会在 Paint 阶段隐藏行。
import { RenderReportRuntimeSurface, createEmptyReportRuntimeViewState, materializeReportTemplate, reduceReportRuntimeViewState, type ReportRuntimeViewIntent,} from 'ds-ui' const documentKey = 'orders-report:v1'let viewState = createEmptyReportRuntimeViewState(documentKey) const rematerialize = () => materializeReportTemplate({ compiled, documentKey, viewState, datasets: {},}) const surface = new RenderReportRuntimeSurface({ document: rematerialize(), onViewIntent: (intent: ReportRuntimeViewIntent) => { const index = surface.interactionIndex if (!index) return const result = reduceReportRuntimeViewState(viewState, intent, index) if (!result.changed) return viewState = result.state surface.document = rematerialize() },})
只有产生运行结构且存在结构后代的普通 Group、Row Axis 和 Column Axis Cell 会显示 −/+
折叠按钮;叶子 Group/Axis 不显示无意义的折叠按钮。成员筛选和排序命令仍属于对应 Group/Axis
Cell 的右侧菜单。Detail、Measure、Aggregate、Subtotal、Total、Grand Total 以及小计/总计标签
都是运行结果,不拥有结构折叠按钮;点击小计金额只执行普通选择或既有 drillthrough,不会隐藏该
Cell。成员筛选弹层支持键盘搜索、全选、反选、清除和应用;候选目录与当前可见节点分离,因而被
目标自身筛选排除的合法成员仍可重新选择。每个 target 最多支持 10,000 个 distinct member;超限时
fail closed,不提供不完整的 Select All。
Runtime sort 覆盖对应 Group/Axis Cell 的模板 orderBy,清除后恢复模板顺序。结构 Collapse 只改变
Group/Axis 后代输出槽位,不改变过滤后聚合作用域,也不隐藏 subtotal/total/grand total。
Measure Sort 菜单使用可读指标标题,解析顺序为静态 Measure Header、Dataset field label、field id、
最后回退到 指标 <Cell address>;同名指标追加 Cell address 区分。measureCellId 只用于稳定 intent
identity 和诊断,不作为 Preview 菜单文案。
collapseAll/expandAll 只操作 Group/Axis 结构;reset 恢复包括兼容 subtotal visibility state 在内
的完整空 ViewState。为保持现有公共 API,collapsedSubtotals/toggleSubtotal 仍可由显式宿主状态
读取和恢复,但默认 RuntimeSurface 不再产生隐藏小计操作。兼容的“显示小计”菜单使用静态小计
标签,或 Aggregate 字段标题/字段名加“小计”,并附加 Cell 地址消歧;subtotalCellId 不进入用户
文案。schema 4 公共 target 使用
placementId + sourceCellId,schema 3 使用全文档唯一 sourceCellId;两者都不暴露 compiler graph id
或 runtime row/column identity。
现有 TSV、Workbook 和 snapshot 从当前 ReportVirtualModel 读取,因此默认导出 current view:筛选后 totals 使用过滤后 rows,已折叠后代不进入 snapshot,且 ViewState/InteractionIndex 不写入导出 metadata。需要 full-data export 时,宿主使用空 ViewState 重新物化。
通用 RenderReportView
RenderReportView 是共享 Canvas 虚拟表格表面,通过 virtualModel 接收已经物化的数据:
import { RenderReportView, compileReportTemplate, materializeReportTemplate, type ReportTemplate,} from 'ds-ui' const template: ReportTemplate = { schemaVersion: 3, rows: [{}], columns: [{}], cells: [], relations: [],}const compiled = compileReportTemplate(template)const model = materializeReportTemplate({ compiled, datasets: {} }).virtualModelconst view = new RenderReportView({ virtualModel: model, dataState: 'ready', visualGuideMode: 'preview', onSelectionChange: selection => { void selection }, onScrollChange: (scrollX, scrollY) => { void scrollX; void scrollY }, onDrillthrough: context => { // 根据 context.datasetName、filters、rowScopes 和 columnScopes 打开业务明细。 void context },}) view.scrollTo(120, 240)view.revealCell(10, 3)view.setSelection( { startRow: 10, startCol: 3, endRow: 10, endCol: 3 }, { scroll: true },)
共享选项由 ReportViewSharedOptions 描述,共享调试结果由 ReportViewDebugState 描述。调试结果包含 scrollX 和 scrollY。ReportDrillContext 和 ReportDrillScopeEntry 可用于读取 materializer 提供的钻取元数据;ReportDrillthroughHandler 是“查看明细”命令的公开处理器类型。只有配置 onDrillthrough 且目标 Cell 具有可用运行时上下文时,默认右键菜单才显示“查看明细”,因此不会出现没有执行目标的空命令。自定义 contextMenuProvider 仍可替换默认菜单。
View 支持:
- 冻结行、左侧列和右侧列;
- 虚拟 viewport 拉取与大数据滚动;
scrollTo()、revealCell()、onScrollChange二维滚动控制;- 单元格选择、键盘移动和复制;
- 多单元格选区使用统一的区域背景和外框,当前活动格使用较弱的内框;
- 行高、列宽调整;
- 自定义右键菜单和显式钻取回调;
- 单行文本真实溢出时显示完整内容 Tooltip;
- loading、empty 和 error 状态。
选择状态与键盘焦点相互独立:失去焦点不会清除已选择区域,但焦点框和键盘移动只在 View 实际获得焦点后生效。dataState 不是 ready 时,View 会保留可供恢复的选择坐标,同时关闭菜单、清除 hover/resize/drag/Tooltip 等瞬态状态,并阻止复制、键盘、指针、滚轮和右键菜单访问旧模型。业务端因此可以安全地在 Loading 或 Error 表面继续持有上一份 virtualModel。
主题与报表文档样式
Report 的工作区、网格头、选择、焦点、hover、resize、冻结边界、设计徽标、诊断和关系辅助视图都从当前 ResolvedTheme 派生。未配置背景、文字或边框颜色的 Cell 分别使用 surfaceDataBody、textPrimary 和 borderData,因此浅色、深色和高对比度主题可以直接作用于同一个 ReportVirtualModel;切换主题不需要重新编译模板或重新物化报表。
普通主题(contrastMode: 'normal')会保留模板或自定义 ReportVirtualModel 明确写入 ReportCellStyle 的背景、文字和边框。屏幕绘制会检查最终背景与文字的对比度:当 Cell 只配置了背景或文字,或者两者组合不可读时,View 仅为本次屏幕绘制选择可读文字色,不会回写模型;背景、边框和可读的显式文字色保持不变。
高对比度主题必须明确设置 contrastMode: 'high'。此时 Report 在屏幕上强制使用主题的 surfaceDataBody、textPrimary 和 borderData,避免模板里的浅色表头、标题或低对比度边框破坏高对比度界面;字体、对齐、内边距以及边框的线型、线宽和显示/隐藏仍按文档样式处理。若业务要求永久改写模板自己的配色,应在模板或物化阶段显式处理,而不是依赖组件主题。
屏幕主题不会写入 ReportTemplate、持久化 JSON 或物化数据。createReportWorkbookExport() 对未配置颜色的 Cell 使用独立的白色纸张、深色文字和浅灰网格默认值,因此高对比度界面不会改变导出文件的文档配色。
正式布局会根据最新内容范围裁剪滚动位置,measure 测量布局不会修改真实滚动状态。替换为新的非空 virtualModel 时,现有选择会尽量按行列坐标保留,并裁剪到新模型范围;将模型设为 null 时会清空选择。
如果自定义 ReportVirtualModel 保持同一个对象引用、但内部行高或列宽发生变化,可调用 invalidateVirtualModel() 重新计算布局,不需要先把 virtualModel 设为 null。
复制与导出
copyVirtualReportRangeToTsv() 和 exportVirtualReportToTsv() 只依赖 ReportVirtualModel,可用于标准 Report、业务自定义模型和大数据模型。createReportWorkbookExport() 用于生成工作簿导出结构。
import { copyVirtualReportRangeToTsv, exportVirtualReportToTsv, compileReportTemplate, materializeReportTemplate, type ReportTemplate,} from 'ds-ui' const template: ReportTemplate = { schemaVersion: 3, rows: [{}], columns: [{}], cells: [], relations: [],}const compiled = compileReportTemplate(template)const model = materializeReportTemplate({ compiled, datasets: {} }).virtualModelconst selected = copyVirtualReportRangeToTsv(model, { startRow: 0, startCol: 0, endRow: 9, endCol: 3,}) const all = exportVirtualReportToTsv(model)
性能建议
- 大数据模型应延迟实现
getCell()和getViewport(),避免预先创建完整矩阵; - 只在 viewport 返回可见 Cell,并保证 row/column span 锚点稳定;
- 数据变化时替换 materialized document 或 virtual model,不要直接修改 View 内部缓存;
- 格式化、聚合和关系计算放在 Report materializer 或业务模型中,Canvas View 只负责绘制与交互。