深色模式
给 AI 的接入说明
这一页是写给代码助手的:把整页贴进上下文,或让它抓 https://report.doc.yijiangsoft.com/md/ai.md,它就能在宿主代码里把接入写对。内容自包含,不用再翻别的页面。
人也能读——只是密度高、没有铺垫。
纯文本索引:
/llms.txt全站正文:/llms-full.txt本页原文:/md/ai.md
TASK
把 conch-report 的查看态页面用 <iframe> 嵌进宿主系统的页面。宿主不引任何包、不改构建;所有输入通过 URL 查询串传入,运行期交互通过 postMessage。
INPUTS(动手前必须确认,缺了就问人,不要编)
| 名称 | 例子 | 没有会怎样 |
|---|---|---|
报表服务地址 ORIGIN | https://report.example.com | 写不出 src |
报表 id ID | r260922123456 | 写不出 src |
| 筛选条件 id 及取值来源 | month ← 页面上的月份选择器;dept ← 登录用户的科室 | 条件传不进去或拼错名字 |
| 哪些条件不让使用者改 | dept | 界面口径不对 |
| 要不要报表自带页头 | 宿主已有标题栏 → 不要 | 页面上出现两条标题栏 |
报表 id 从报表服务的报表列表页(每行的「嵌入」按钮)或 GET /api/reports 取。
HARD RULES
- 只嵌查看态:
view=1必须有。少了它嵌进去的是设计器(带对话和属性面板),是错的。 - 报表必须已发布。没发布的报表打开只显示一句「还没有发布」;发布由做报表的人在报表服务里点,宿主侧做不了。
- 不要在链接里放 token、密码、签名、SQL。链接会进历史记录和 Referer。报表服务也不接受从客户端传 SQL。
lock不是权限。它只把控件设成只读,改链接就能绕开。按身份限制数据必须在报表服务侧用行级范围参数做,不能靠lock交差;写代码时如果用户以为lock能限权,要明确告诉他不能。- 条件变化走
postMessage,不要重新拼src。换src会让 iframe 整页重载、图表重画一遍。首次挂载用 URL,之后用setParams。 postMessage的targetOrigin写具体域名,不要'*';接收侧校验e.origin和msg.source。- 不要替用户猜条件 id。猜错的表现是「页面上多一条提示、筛选没生效」,比报错更难被发现。
URL CONTRACT
{ORIGIN}/?id={ID}&view=1[&<条件id>=<值>][&lock=…][&chrome=0][&version=…][&theme=…]保留字段(其余 key 一律按报表筛选条件解析):
| key | 值 | 含义 |
|---|---|---|
id | 报表 id | 必填 |
view | 1 | 必填,查看态 |
version | v3 / draft / 省略 | 省略=当前发布版;draft 仅供发布前自查,不要给客户 |
lock | a,b 或 all | 这些条件只读 |
chrome | 0 | 去掉报表页头(标题、版本、刷新、导出) |
theme | primary:%232c6ecb,bg:%23ffffff | 覆盖设计令牌;名字省略 --cr- 前缀;值里不能有逗号 |
条件值的写法(先 encodeURIComponent,用 URLSearchParams 就自动做了):
| 条件类型 | 写法 | 例 |
|---|---|---|
| 文本 / 数字 / 日期 / 月份 / 单选 / 树 | id=值 | month=2026-08、day=2026-08-31、store=S001 |
| 日期区间 | id=起,止 或 id_from=/id_to= | range=2026-01-01,2026-03-31 |
| 数字区间 | id=小,大 或 id_min=/id_max= | amount=100,500 |
| 多选 | id=a,b 或重复 id=a&id=b | store=S001,S002 |
| 不限(清掉报表默认值) | id=(空串) | month= |
链接上不写的条件 = 用报表自己的默认值。写了但报表里没有的 key 会被忽略,页面顶上列出来提示。
POSTMESSAGE CONTRACT
报表 → 宿主,{ source: 'conch-report', type, … }:
| type | 附带字段 | 用途 |
|---|---|---|
ready | id, title, version | 渲染完成;此前发给报表的消息会被丢掉 |
height | height(px) | 内容高度变化;宿主据此调 iframe 高度,避免第二条滚动条 |
params | params(全部条件的当前值) | 使用者或宿主改了条件 |
宿主 → 报表,{ source: 'conch-report-host', type, … }:
| type | 附带字段 | 用途 |
|---|---|---|
setParams | params:条件 id → 值(只写要改的) | 改条件并重新取数 |
refresh | — | 按当前条件重新取数 |
setTheme | tokens:令牌名 → 值 | 换配色 |
setParams 的值用 JS 原生类型:区间 ['2026-01-01','2026-03-31']、多选 ['S001','S002']、不限 null。
REFERENCE IMPLEMENTATION(框架无关,照这个结构写)
js
function mountReport(container, { origin, id, params = {}, lock, chrome = true, version }) {
const q = new URLSearchParams({ id, view: '1' })
for (const [k, v] of Object.entries(params)) {
if (v !== null && v !== undefined) q.set(k, Array.isArray(v) ? v.join(',') : String(v))
}
if (lock) q.set('lock', Array.isArray(lock) ? lock.join(',') : lock)
if (chrome === false) q.set('chrome', '0')
if (version) q.set('version', version)
const frame = document.createElement('iframe')
frame.src = `${origin}/?${q}`
frame.style.cssText = 'width:100%;height:480px;border:0'
container.appendChild(frame)
let ready = false
const queue = []
const post = (msg) => {
if (!ready) { queue.push(msg); return } // ready 之前排队,别丢
frame.contentWindow.postMessage({ source: 'conch-report-host', ...msg }, origin)
}
const onMessage = (e) => {
if (e.origin !== origin || e.source !== frame.contentWindow) return
const msg = e.data
if (!msg || msg.source !== 'conch-report') return
if (msg.type === 'height') frame.style.height = msg.height + 'px'
if (msg.type === 'ready') { ready = true; queue.splice(0).forEach(post) }
}
window.addEventListener('message', onMessage)
return {
setParams: (p) => post({ type: 'setParams', params: p }),
refresh: () => post({ type: 'refresh' }),
setTheme: (t) => post({ type: 'setTheme', tokens: t }),
destroy: () => { window.removeEventListener('message', onMessage); frame.remove() },
}
}Vue 3 / React 的成品组件见宿主代码示例(原文 /md/guide/examples.md),结构与上面一致:src 只在挂载时算一次,条件变化 watch/useEffect 里发 setParams,height 存进组件状态。
HTTP API(只有在不嵌 iframe、要自己画的时候才用)
| 方法 | 路径 | 请求体 | 回什么 |
|---|---|---|---|
| GET | /api/reports | — | 报表列表,publishedVersion 为 null 表示没发布 |
| GET | /api/reports/{id}/view?version= | — | 规格(不含 SQL)+ 初始条件 +(快照版的)数据;409 = 没发布 |
| POST | /api/reports/{id}/data | {version, params} | {queries:{<查询名>:{columns,columnTypes,rows,truncated,elapsedMs}}},单条失败带 error |
| POST | /api/reports/{id}/drill | {version, drill, args, params} | 穿透明细 |
| POST | /api/reports/{id}/export/xlsx|docx | {version, params[, drill, args]} | 文件流 |
| GET | /api/reports/{id}/versions | — | 版本列表 |
错误统一 {"error":"…"}。多租户部署时租户来自请求头 X-Conch-Tenant,由反向代理注入,前端传不了。
CHECKLIST(交付前逐条核对)
- [ ]
src里有id和view=1 - [ ] 条件 id 是从报表里确认过的,不是猜的
- [ ] 条件值用
URLSearchParams拼,没有手工字符串拼接 - [ ] 监听了
height并调整 iframe 高度 - [ ] 校验了
e.origin与msg.source;postMessage的 targetOrigin 不是'*' - [ ]
ready之前发的消息有排队补发 - [ ] 条件变化走
setParams,没有重拼src - [ ] 组件卸载时移除了
message监听 - [ ] 宿主是 https 时报表地址也是 https
- [ ] 跟用户确认过:
lock不等于数据权限
FAILURE MODES
| 现象 | 原因 | 处理 |
|---|---|---|
| 页面写「还没有发布」 | 报表只有草稿 | 让做报表的人发布;自查可用 version=draft |
| iframe 空白 | 混合内容 / frame-ancestors 不含宿主域名 / 代理加了 X-Frame-Options / sandbox 太紧 | 看浏览器控制台的拒绝原因,对号处理;sandbox 至少要 allow-scripts allow-same-origin,导出还要 allow-downloads |
| 条件没生效,顶上有提示 | 条件 id 拼错 | 核对报表筛选栏里的 id |
| 条件没生效,没有提示 | 值的写法不对(区间、多选、编码) | 按上面的写法表改 |
| 出现第二条滚动条 | 没监听 height | 加上高度同步 |
setParams 无反应 | 发早了 / source 写错 / targetOrigin 不匹配 / 该条件被 lock 或快照锁了 | 按顺序排查 |
| 报表改了页面没变 | 只改了草稿没发布,或链接钉了 version | 重新发布;去掉 version |
GLOSSARY
- 查看态:只有筛选栏和画布的只读页面,
view=1,宿主嵌的就是它。 - 设计器:做报表的界面,带对话面板和属性面板,不要嵌给最终用户。
- 发布版 / 草稿:嵌出去的链接跟发布版走;草稿是设计者正在改的那份。
- 数据快照:发布时固化了数据的版本,打开不查库,条件只读。
- 穿透明细:点图表或指标卡弹出的明细层,查看态里点了就有,宿主不用管。
- 设计令牌:
--cr-*开头的 CSS 变量,主题只换值不改样式源码。
