顶层结构(envelope)详解
导出/导入的工程 body 来自编辑器 toProjectBody(),形状由 SDK VpProjectBody 定义。导入时 project 喂给 new VpProject(project, init),其余喂给运行时数据层。
0. 一句话规则
- 节点 = 一棵
VpNode树(页面/组件/piece 同构)。 - 导出不做"去默认值压缩":
options里有什么就导出什么;没写的项在运行时由蓝图默认值回落。所以手写时只写需要的项即可,但position/size建议显式写。
1. body 顶层
{
"project": { /* 必填,见 §2 */ },
"connections": [], // 可选,缺省 []
"connectionData": {}, // 可选,缺省 {}
"dataRules": [], // 可选,缺省 []
"projectParams": {} // 可选;千人千面参数,一般不写(project.params 已够)
}
类型(SDK VpProjectBody):
interface VpProjectBody {
project: VpProjectModel
connections?: VpConnection[]
connectionData?: VpConnectionData // Record<connUid, VpDataTable[]>
dataRules?: unknown[]
projectParams?: Record<string, unknown>
}
2. project
{
"name": "工程名", // 可选
"screen": { /* 主页面,必填 */ },
"screens": [ /* 其余普通页面,可选 */ ],
"foregroundScreen": { /* 前景层,可选 */ },
"backgroundScreen": { /* 背景层,可选 */ },
"windows": [ /* 窗口(弹窗/抽屉/锚定窗口),可选,见 §2b */ ],
"params": {}, // 项目参数(千人千面);一般 {}
"settings": {} // 工程级设置(工具栏/访问/访客/演示/sizeModes 断点…);一般 {}
}
- 导出时把第一个普通页面放
screen,其余放screens[];导入时再合并回VpProject.screens顺序。 - 前景/背景层也用
type:"screen",区别只在它们挂在foregroundScreen/backgroundScreen键上。 - 最小可导入 project:
{ "screen": { "uid":"screen_main", "type":"screen", "widgets":[] } }。
类型(VpProjectModel):id?、name?、screen(必填)、foregroundScreen?、backgroundScreen?、screens?、windows?、params?、settings?、version?。
2b. 窗口 windows(工程级弹窗/抽屉/锚定窗口)
project.windows[] 是与页面平级的顶层实体(对照 ExB config.dialogs):节点 type:"window"(结构同 screen,内容放 widgets),由 switchWindow 动作(windowUid + operate: open/close/toggle)在播放态开关,或用 window-instance 组件静态嵌入页面复用。不用窗口就省略整个键。地图图层的 feature-panel-uid(点击要素弹锚定窗口)引用的正是这里的窗口 uid。
窗口节点关键 options:
| option | 默认 | 说明 |
|---|---|---|
window-mode |
fixed |
fixed 固定位置 / anchored 锚定(贴触发组件弹出,地图点位触发时跟随地图) |
window-position |
center |
固定窗口五锚点:center/top/bottom/left/right(顶/底只高度生效宽=页宽,左/右只宽度生效) |
size + size-unit |
— / px |
窗口大小;单位 px/percent(相对舞台) |
window-offset / window-margin |
[0,0] / [0,0,0,0] |
位置偏移;贴边窗口铺满轴两侧留白(上下左右) |
block-page + background-mask-color |
true / 半透黑 |
模态:激活期间页面不可交互+遮罩(锚定窗口不适用) |
close-outside |
true |
点遮罩/画面外关闭 |
titlebar-show + titlebar-text/height/bg/color/font-size/draggable |
false |
内置标题栏(留空 text=窗口名;固定窗口播放态可拖动,位置不持久化) |
close-btn-show + close-btn-color/size |
true |
关闭按钮 |
锚定:anchor-placement/anchor-gap/anchor-arrow |
auto/8/true |
弹出朝向(auto 自动翻转)/锚点间距/指向箭头 |
window-bg / box-shadow-* / show-scroll |
#1b1f27 / 关 / false |
背景色/框阴影/内容滚动 |
打开方式除 switchWindow 外:页面 open-with-window(进页自动开,一次会话一次)、工程 settings.splashWindowUid(启动闪屏,至多一个)、SDK ctx.openWindow(uid)(宿主程序化开关)。多窗口按打开顺序成 z 序,Esc 关最顶层。
3. 通用节点 VpNode
所有页面/组件/piece 都是这一种结构:
interface VpNode {
uid: string
type: string
name?: string
enabled?: boolean // piece 用,缺省 true
locked?: boolean
options?: Record<string, any>
stateOverrides?: Record<string, Record<string, any>>
sizeModeOverrides?: Partial<Record<'MEDIUM'|'SMALL', Record<string, any>>> // 设备差异覆盖(LARGE=基线 options)
states?: VpState[]
state?: string
widgets?: VpNode[] // 容器(页面/分组面板)的子组件
pieces?: VpNode[] // builder(地图/组态)的子元素
unitTemplate?: VpNode // 仅 data-repeater:单元模板(group-panel 节点)
pending?: VpNode[] // 仅页面根:待处理暂存区(不渲染,编辑器「加回画布」用)
privateData?: VpPrivateData // 组件私有数据(表格默认示例数据用)
conditionalStyles?: VpConditionalStyle[]
extensions?: VpExtension[] // 二次开发脚本(代码面板):{uid,name,enabled,code}
guideLines?: Record<string, unknown>
}
手写时通常只用到:uid、type、name?、options、(容器)widgets、(地图/组态)pieces、(数据循环容器)unitTemplate、(多状态)stateOverrides、(设备差异)sizeModeOverrides。component-instance 节点另有顶层 masterId/masterVersion/props/overrides/master(见 widgets/containers.md §5bb)。
4. 页面节点 screen
{
"uid": "screen_main",
"type": "screen", // 恒为 screen
"name": "主页面", // 可选
"options": {
"fullscreen-mode": "stretch", // 铺满屏幕;不写则默认 scaling 会留黑边
"size": [1920, 1080], // 画布尺寸 [宽,高]
"background-color": "#0c0d0e" // 背景色
},
"stateOverrides": {},
"widgets": [ /* 顶层组件 */ ]
}
页面默认 options(省略即用默认):
| option | 默认 | 说明 |
|---|---|---|
layout-profile |
推断 | 布局画像主声明(canvas-scale固定大屏 / canvas-percent百分比自适应 / flow-fluid流式),生成工程时必写(见 visual-style.md §3 铺满守则、本文 §4.1) |
size |
[1920,1080] |
画布大小 [宽,高] |
background-color |
#1c222a |
背景色(新建工程模板常设 #0c0d0e) |
fullscreen-mode |
scaling |
全屏适配方式(见下) |
fullscreen-fill-color |
#000000 |
适配留白色(仅 scaling 留边时可见) |
fill-auto-expand |
false |
仅 fill-uniform/responsive:把无显式锚点的主视觉拉满剩余画布(见 4.1) |
container-layout |
static |
static/fluid/grid |
carousel-animate |
false |
页面轮播动画 |
响应式断点:project.settings.sizeModes.breakpoints 声明 SMALL/MEDIUM 宽度阈值,形状 "settings": { "sizeModes": { "breakpoints": { "small": 767, "medium": 1279 } } }(视口宽 ≤small→SMALL、≤medium→MEDIUM、否则 LARGE;缺省即此默认,非法/small≥medium 回退默认;同级 widths/heights 只管编辑态预览画布;配合节点 sizeModeOverrides 与 device-visible 做设备差异化,示例见 data/组件/入门/28)。设备档位对所有适配方式生效(固定画布也按视口宽切档),但固定画布页里 position/size 不参与设备差量——写进 sizeModeOverrides 也不生效,几何由整屏缩放 + 锚点/自动填充负责;可按设备覆盖的是 device-visible、字号配色、容器 grid-columns/fluid-wrap/container-layout、文案等。
4.1 fullscreen-mode(全屏适配方式)—— 让大屏铺满屏幕 ⚠️
运行/预览态由运行时的 useStageScaling 按激活页面的 fullscreen-mode 把画布缩放进视口:
| 值 | 含义 | 铺满? | 取舍 |
|---|---|---|---|
scaling(默认) |
等比缩放 contain:min(cw/bw,ch/bh) 居中 |
❌ 视口比例≠画布时留黑边 | 保比、不裁不变形 |
fill-uniform |
等比铺满(左上·不变形):等比 min 缩放锚左上,画布撑大到视口、右/下空白由页面背景延展填充(仿 autofit) | ✅ | 不变形、内容完整、无黑边;内容锚左上不居中,空白区只有背景(背景为纯色/渐变时视觉无缝)——「不变形又要铺满」的正解 |
adaptive |
铺满裁切 cover:max(...) 居中溢出裁剪 |
✅ | 保比,但裁掉贴边内容 |
stretch |
拉伸:X/Y 独立铺满 | ✅ | 所有内容都在;真 16:9 屏与画布同比时无变形,偏离比例才拉伸变形 |
responsive |
页面尺寸跟随容器、内容不缩放回流 | ✅(尺寸) | 固定坐标布局会错位,配百分比/流式用 |
fit-width / fit-height |
按宽/高等比铺满,另一轴溢出滚动 | ✅(单轴) | 竖屏长页门户 / LED 横带条屏 |
original |
1:1 不缩放居中 | ❌ | 仅调试/特殊场景 |
- 缺省
scaling不占满屏幕:因为默认画布是 16:9(1920×1080),而预览/发布视口(带工具条、非 16:9 显示器、窗口化)常不是 16:9,于是 contain 留黑边。 - 结论:要铺满,就在每个
type:"screen"节点(含screens[]/foregroundScreen/backgroundScreen)的 options 显式写fullscreen-mode。固定画布(container-layout:"static")默认用"fill-uniform"+"fill-auto-expand":true(不变形+铺满,主视觉吃掉撑开的画布,见data/布局/适配/13~15)。"stretch"会把圆拉成椭圆,只留对照例data/布局/适配/16。要裁贴边用"adaptive"。 - 流式/百分比页面(
container-layout:"fluid"或画像flow-fluid/canvas-percent)只能写"responsive":画布=视口、内容回流,fill-uniform对它们无意义(fill-auto-expand也只对static生效)。
编辑器的新建工程模板起手式已默认 "fullscreen-mode":"fill-uniform" + fill-auto-expand;但手写/生成的 JSON 不经过模板——生成工程时仍须显式写 fullscreen-mode。
fill-uniform 让内容吃掉扩展区(不变形 + 不留空)
fill-uniform 与 scaling 的内容倍率相同,差别是留白成了真实画布(画布 = 视口 ÷ 缩放系数,多出的在右/下)。谁去占用它有两种写法,优先第一种:
- 锚点(精确):主视觉写
"responsive-x":"left-right","responsive-y":"top-bottom"(四边贴边),侧栏写"responsive-x":"right","responsive-y":"top-bottom"。锚点原先只在responsive下生效,现在fill-uniform同样生效。 - 自动填充(兜底):页面写
"fill-auto-expand": true,把没有显式锚点的map-2d/scene-3d/scada-2d/basic-table/basic-list/deco-border/布局容器,以及够大的basic-card/chart-*(宽 ≥ 画布 15%、高 ≥ 10%)拉到剩余空间,并夹在包裹它的外框、同宽顶栏、右侧与下方邻居之内。外框若也是填充候选,会先撑开,再让内部主视觉跟到新内缘(组态套deco-border时才能铺满)。贴着设计稿右缘的窄栏(距右约 96px 内的状态卡)随画布变宽右移,与贴边栏水平重叠的同侧小控件作为侧栏簇一起走;横排 KPI 末卡不搬(不依赖是否有装饰边框)。说明顶栏比主视觉宽但右侧还有贴边控件时,主视觉吃到控件左缘。侧栏也可写responsive-x: right。自定义组件可用meta.canvasFill声明是否候选。
主视觉被四周固定件包围的通用配方(左右面板 + 顶/底工具条,见 data/布局/适配/20):两侧面板写 "responsive-x":"left"/"right" + "top-bottom",底部按钮排写 "responsive-x":"left-right" + "responsive-y":"bottom",中间主视觉不写锚点——锚点先跑,自动填充再把主视觉夹进「右侧邻居左缘、下方邻居上缘」。按钮排要按钮跟着变宽重排就用 layout-flow-row(带主轴对齐项,fluid-wrap:"nowrap" + fluid-justify-content:"space-between";group-panel 只有换行开关)。
两者共存时锚点优先。容器不想被撑大(要卡片数不变)就写 "canvas-grow":"scale"——但它只管尺寸不管位置:这种容器要放在画布增长的背面(靠里一侧)让主视觉吸收增量,摆在右缘/底缘会留一块没人占的背景空白;要贴边又不变大就用锚点 "responsive-x":"right"。只写运行态几何,不改 options、不进历史。
5. 组件节点 widget
{
"uid": "w_bar1", // 任意唯一串(一般无前缀)
"type": "chart-bar", // 见 widgets/index.md 注册类型
"name": "销售柱图", // 可选;省略用组件默认名
"options": {
"position": [100, 100], // 左上角 [x,y],默认 [0,0]
"size": [420, 300], // [宽,高],默认随组件
"category-field": [["conn_x","tbl_x","月份"]],
"metric-fields": [["conn_x","tbl_x","销售额"]]
},
"stateOverrides": {}
}
name仅在你想固定显示名时写;否则按defaultName(如柱状图取title或回落"柱状图")。- 容器型(
group-panel)把子组件放widgets: [],子组件position相对容器左上角。 - builder 型(
map-2d)把本体/图层放pieces: [](见 §7)。
6. stateOverrides(多状态)
- 默认状态的值放
options;额外状态的差异覆盖放stateOverrides[stateUid]。 - 不用多状态:给
{}或直接省略stateOverrides/states。 stateOverrides["s_x"]: {}表示状态s_x存在但无覆盖(读取回落options)。没有stateOverrides[""]这种写法(默认态永远读options)。
绝大多数手写工程不需要多状态,留 {} 即可。
7. piece 节点(仅 builder 型,如 2D 地图)
{
"uid": "map-body",
"type": "map-body",
"name": "场景",
"enabled": true, // 缺省 true
"options": { "coordinate-system": "geographic", "basemap": "gaode-vec", "map-center": [116.4,39.9], "map-zoom": 4 }, // coordinate-system 缺省为 "cad",不写则 basemap 被忽略、渲染空白 CAD 图
"stateOverrides": {}
}
map-2d的pieces数组第一个应是map-body(本体,单例不可删,建议uid:"map-body")。- 其余 piece 为图层:
map-tile/map-marker/map-geojson(见 widgets/map-2d.md)。 - piece 可再嵌
pieces(一般用不到)。
8. options 序列化的真相(重要)
- 编辑器不在导出时和默认值比对裁剪 ——
node.options里存在的键就原样导出,被"重置"过的键才会被删掉。 - 因此真实导出文件是稀疏混合:新建组件会带
size/position等少量键,其余走默认。 - 手写策略:只写需要偏离默认的项 + 必要的
position/size+ 字段绑定。其它一律省略,交给默认值,文件更短更稳。 - 想知道某项默认值:查 widgets/index.md 对应家族文档,或自动生成的 widget-schema.generated.json。
9. 特殊 option 值形态(少见,按需)
- 元素引用:
{ "refType":"element", "targetPath":[screenUid, widgetUid?, pieceUid?] }(link-section/target-map/window等引用型 option 与动作目标都用它)。 - 文件引用:
{ "refType":"file", "relativePath":"...", "url":"..." }。 - 交互(
options.interactions):{ "version": 2, "groups": [...] },分组按 trigger 内容匹配(v1 哈希键形态已废弃);完整写法见 interactions.md。