宿主事件与方法调用
嵌入报表与宿主页面之间的 postMessage 协议:接收加载、错误、查询、单元格点击等事件,向报表发送 invoke 调用参数、刷新、导出。
概述
报表嵌入 iframe 后,可以和宿主页面双向通信:
- 事件(报表到宿主):报表加载完成、出错、查询、导出,以及设计者在报表里配置的自定义事件(点击单元格、点击图表数据点等)。
- 方法调用(宿主到报表):宿主向 iframe 发送
invoke消息,改变查询参数、重新查询、读取状态、导出等。
所有消息都通过 window.postMessage 传递,消息体中带 protocol: 'sight-report'。宿主接收时必须校验 event.origin(报表系统的部署地址)和 data.protocol,避免误收页面上其他组件的消息。
功能入口
- 自定义事件的配置位置(在设计器中):
- 网格报表:单元格 > 链接 > 发送事件。
- 单据:组件 > 定位与操作 > 点击链接。
- 仪表盘:区块 > 点击数据点 / 点整行 / 按列配置 > 发送事件到宿主页面,见 仪表盘的联动与钻取。
- 「三方集成」页面的「接入说明」弹窗中有「接收报表事件」代码示例。
事件(报表到宿主)
消息格式
{
"protocol": "sight-report",
"name": "report:loaded",
"payload": { "elapsedMs": 812 },
"source": { "reportId": "rpt_sales" },
"context": { "parameters": { "year": 2025 } },
"timestamp": 1790658966000
}
| 字段 | 说明 |
|---|---|
protocol |
固定为 sight-report |
name |
事件名。系统事件以 report: 开头,自定义事件为设计者填写的名称 |
payload |
事件数据 |
source |
事件来源定位,reportId 必有;网格有 cellName、cid;单据有 componentId、componentType、rowIndex;仪表盘有 blockId、blockType、tabId。按事件名分发后只读需要的字段即可 |
context |
仅自定义事件带:当前生效的查询参数 { parameters }(仪表盘为过滤栏的生效值,含日期范围展开与联动叠加) |
timestamp |
毫秒时间戳 |
系统事件
| 事件名 | 发送条件 | payload |
|---|---|---|
report:loaded |
首次加载渲染完成,只发一次;直接发送 | { elapsedMs };仪表盘在首屏区块取数完成时才发,并多带 blockCount、failedCount |
report:error |
加载、查询失败;直接发送 | { phase: 'load' | 'query', message } |
report:query |
发起查询;需订阅 | { parameters };仪表盘另带 trigger: 'user' | 'auto' |
report:query-done |
每次查询渲染完成(含首次);需订阅 | { parameters, elapsedMs };仪表盘另带 trigger: 'user' | 'auto'(定时刷新也会发)和 failedCount |
report:export |
用户触发导出;需订阅 | { format },取值 excel、pdf、word、csv、ofd |
report:print |
用户或宿主触发打印;需订阅 | { command, jobId },宿主传了打印机 / 份数时另带 printer、copies |
report:print-done |
一次打印结束,成功和失败都发;直接发送 | { jobId, command, mode, printer, copies, success, error?, spoolJobIds?, fallbackReason? },见下文「打印机、份数与打印结果」 |
report:resize |
内容高度变化,用于 iframe 高度自适应;需订阅 | { height } |
report:print-progress、report:print-continued、report:print-cancelled |
续打相关;需订阅 | 续打状态与位置 |
「直接发送」的事件(report:loaded、report:error、report:print-done 和自定义事件)宿主监听即可收到;「需订阅」的过程性事件需要先向 iframe 发一条 subscribe 消息。
订阅过程性事件
const iframe = document.getElementById('report');
iframe.contentWindow.postMessage(
{ protocol: 'sight-report', type: 'subscribe', events: ['report:query-done', 'report:resize'] },
'https://your-domain'
);
// 订阅全部:events: ['*']
订阅同时会锁定宿主 origin,此后报表发送的事件使用精确的 origin,安全性更高。未订阅时,报表用 document.referrer 的 origin 作为目标;referrer 也为空时以 * 发送。
接收事件
window.addEventListener('message', (event) => {
if (event.origin !== 'https://your-domain') return; // 报表系统的部署地址
const data = event.data;
if (!data || data.protocol !== 'sight-report') return;
if (data.name === 'report:loaded') {
console.log('报表已就绪', data.payload);
} else if (data.name === 'patient-selected') { // 设计者配置的自定义事件
console.log('选中患者', data.payload, '当前参数', data.context && data.context.parameters);
}
});
注意:自定义事件名不能以
report:开头,该前缀保留给系统事件;使用了该前缀的事件在运行时会被丢弃。
方法调用(宿主到报表)
消息格式
请求:
{ "protocol": "sight-report", "type": "invoke", "id": "req-1", "method": "setParameters", "args": [{ "year": 2025 }] }
响应(回送给发起方窗口):
{ "protocol": "sight-report", "type": "invoke-result", "id": "req-1", "ok": true, "data": { "queried": true, "success": true } }
失败时为 { ..., "ok": false, "error": "..." }。id 必须是非空字符串,用于匹配请求与响应。invoke 只在报表运行于 iframe 内时响应,直接打开报表页不响应。
方法清单
| 方法 | 参数 | 说明 |
|---|---|---|
setParameters |
(params, { query }) |
合并查询参数;query 不为 false 时立即重新查询并等待完成 |
getParameters |
(names?, { raw }) |
读取参数;names 省略则返回全部;raw 仅对仪表盘有区别(true 取控件原始值,缺省取含日期展开与联动叠加的生效值) |
query |
无 | 重新查询 |
reset |
无 | 重置查询条件 |
export |
(format) |
发起导出,受理即返回 { accepted: true },没有完成信号 |
print |
(command?) 或 ({ printer, copies, command }) |
发起打印,受理即返回 { accepted, jobId },结果看同一 jobId 的 report:print-done;仪表盘不支持 |
getState |
无 | 读取状态,含 reportId、fileType、loading 等;网格另含 parameters、variables、currentPage、totalPages、activeSheetId;仪表盘另含 activeTabId、parameters、crossFilter、lastUpdatedAt |
setSheet |
(sheetId) |
切换多 sheet 报表的页签;仪表盘不支持 |
getCellValue |
(cellName) |
读取首个同名单元格渲染后的显示值,返回 { cid, text },取不到返回错误;仅网格支持 |
getCellValues |
(cellName) |
读取同名单元格的全部实例,返回 { cid, text } 数组;仅网格支持 |
getTabs、setTab |
(tabId) |
仪表盘页签;仅仪表盘支持 |
getPrintProgress、continuePrint |
(options?) |
续打进度与续打;仅网格支持 |
在报表首次加载完成前,setParameters、query、reset、export、print、setSheet、continuePrint 返回 error: 'not-ready',宿主应等 report:loaded 后重试;读取类方法始终放行,getState().loading 可作为就绪探测。不存在的方法返回 unknown method: xxx;当前报表类型不支持的方法返回 unsupported: …,不会静默成功。
打印机、份数与打印结果
宿主可以在调用时指定打印机和份数(网格与单据都支持),并通过 report:print-done 拿到这一次打印的结果,用于打印轨迹:
const { jobId } = await invoke('print', { printer: '导引单-1F', copies: 2 });
// 稍后收到:
// { name: 'report:print-done',
// payload: { jobId, command: 'directPrintAll', mode: 'plugin', printer: '导引单-1F', copies: 2,
// success: true, spoolJobIds: ['118'] } }
| 参数 | 说明 |
|---|---|
printer |
打印机的系统名或显示名(与本机打印服务 magic-print 枚举出的一致) |
copies |
份数,1–999 的整数 |
command |
可选:previewPrintAll、previewPrintCurrent、directPrintAll、directPrintCurrent。不传时,传了 printer 或 copies 就按 directPrintAll(静默打印全部),否则按 previewPrintAll |
- 经 magic-print 静默打印时使用宿主传入的值,没传的项用报表「打印设置」里的配置。宿主传了
printer时不看报表的「打印预览」开关,直接静默打印。 - 显式指定
previewPrint*命令时弹出系统打印对话框,浏览器不接受页面指定打印机和份数,printer、copies不生效。 - 参数不合法(份数越界、命令名未知等)时调用失败,
error以invalid-argument:开头。 - 工具栏触发的打印同样会发
report:print和report:print-done,各自带jobId。
report:print-done 的 payload:
| 字段 | 说明 |
|---|---|
jobId |
与 print 调用返回值、report:print 事件中的相同 |
mode |
plugin:经 magic-print 静默打印;dialog:系统打印对话框 |
printer、copies |
实际使用的打印机和份数;dialog 模式下浏览器不告知页面,为 null |
success |
plugin:打印机已受理(任务进入系统打印队列);dialog:打印对话框已关闭。浏览器不区分「打印」和「取消」,对话框关闭即视为完成 |
error |
失败原因,例如本机打印服务不可用、取 PDF 失败 |
spoolJobIds |
magic-print 返回的系统打印队列任务号 |
fallbackReason |
指定的打印机不存在、已脱机或插件打印失败时,报表会改为弹出打印对话框,这里记录原因。宿主据此判断这次打印没有送到指定的打印机 |
使用宿主 SDK sight-report-embed.js 时,旧版 SDK 也能直接调用 report.print({ printer, copies }) 并监听 report:print-done(该事件直接发送,不需要订阅)。SDK 的类型声明(sight-report-embed.d.ts)对应如下:
export type PrintCommand = 'previewPrintAll' | 'previewPrintCurrent' | 'directPrintAll' | 'directPrintCurrent'
export interface PrintOptions {
printer?: string
copies?: number
command?: PrintCommand
}
export interface PrintDonePayload {
jobId: string
command?: PrintCommand
mode: 'plugin' | 'dialog'
printer: string | null
copies: number | null
success: boolean
error?: string
spoolJobIds?: string[]
fallbackReason?: string
}
// SightReportInstance
print(commandOrOptions?: PrintCommand | PrintOptions): Promise<{ accepted: boolean; jobId: string }>
示例
页面加载后按用户所在科室过滤报表,并在查询完成后自适应 iframe 高度:
const iframe = document.getElementById('report');
const ORIGIN = 'https://your-domain';
let seq = 0;
function invoke(method, ...args) {
return new Promise((resolve, reject) => {
const id = 'req-' + (++seq);
const onMsg = (e) => {
const d = e.data;
if (e.origin !== ORIGIN || !d || d.protocol !== 'sight-report'
|| d.type !== 'invoke-result' || d.id !== id) return;
window.removeEventListener('message', onMsg);
d.ok ? resolve(d.data) : reject(new Error(d.error));
};
window.addEventListener('message', onMsg);
iframe.contentWindow.postMessage({ protocol: 'sight-report', type: 'invoke', id, method, args }, ORIGIN);
});
}
window.addEventListener('message', async (e) => {
if (e.origin !== ORIGIN || !e.data || e.data.protocol !== 'sight-report') return;
if (e.data.name === 'report:loaded') {
iframe.contentWindow.postMessage(
{ protocol: 'sight-report', type: 'subscribe', events: ['report:resize'] }, ORIGIN);
await invoke('setParameters', { dept: '内科' });
} else if (e.data.name === 'report:resize') {
iframe.style.height = e.data.payload.height + 'px';
}
});
注意事项
注意:一个 iframe 对应一张报表。报表内部钻取出来的子报表实例不参与宿主通道,宿主只会收到主报表的事件,也只有主报表响应
invoke。
提示:
export与report:error;打印的结果看report:print-done。
提示:宿主页面使用 Vue 组件(而非 iframe)直接嵌入报表视图时,事件通过组件的
report-event事件全量触发,不需要订阅。