VP-TSX:用 TSX 生成工程 JSON(默认产出路径)
大模型直接吐几千行工程 JSON 容易闭合错误、重复段落膨胀、属性名臆造。默认用 VP-TSX:写一份 *.vp.tsx(组件面 = 平台 widget 目录,类型自动生成自 widget schema),执行即产出可导入的 { project, connections, connectionData, dataRules }。只有微型工程(一两个组件的片段)才值得直接手写 JSON。
红利:类型即时纠错(值/枚举/元组 tsc 报错,臆造选项名由 compile 对照 schema 拦截并给相近候选);循环/变量/函数消除重复(8 张 KPI 卡 = 一个 map());可写注释。
边界:本文是 TSX 写法权威(离线从零生成 + 读懂导出代码)。live 页面双向环路(page_export_tsx / page_apply_project)的工作流见 page-live-loop.md。真源始终是页面 JSON:TSX 是每次导出即再生的代码视图。只改一两个属性仍是 page_get_schemas → page_batch_set_options 最快。
产出链(在 vjvp/ 所在目录执行)
# 0. schema 变更后重新生成类型(types.generated.ts 已入库,通常跳过)
pnpm exec tsx vjvp/scripts/vptsx/gen-types.mts
# 1. 类型检查(值/枚举/元组错误在这里报,带行号)
# ⚠️ tsc -p vjvp/scripts/vptsx/tsconfig.json 只查 runtime.ts + scripts/vptsx/examples/(include 写死),查不到你的文件;
# 检查自己的 .vp.tsx 用单文件命令(命令行给文件时 tsconfig 被忽略,选项须写全):
pnpm exec tsc --noEmit --strict --skipLibCheck --jsx react --jsxFactory h --jsxFragmentFactory Fragment --target ES2022 --module ESNext --moduleResolution Bundler <你的工程.vp.tsx>
# 2. 执行 + 选项名检查 + 校验器终审(默认写 <输入名>.vpproj.json)
pnpm exec tsx vjvp/scripts/vptsx/compile.mts <你的工程.vp.tsx>
ERROR 必须修掉再交付。新写的 *.vp.tsx 放哪都行(相对导入 runtime 即可);技能自带示例在 scripts/vptsx/examples/energy-mini.vp.tsx,先照抄骨架。外部工作区(技能被拷走)需 npm i -D tsx typescript。
文件模板(必须的头两行 pragma + default export)
/** @jsx h */
/** @jsxFrag Fragment */
import { bind, defineProject, h, jsonConnection } from '<相对路径>/scripts/vptsx/runtime'
export default defineProject({
name: '工程名',
connections: [/* jsonConnection(...).connection 或原始连接对象 */],
connectionData: {/* ...jsonConnection(...).connectionData(仅离线 compile.mts);live page_apply_project 写字面量,见 page-live-loop.md */},
screen: (
<screen uid="screen_main" name="主页面" layout-profile="canvas-scale" fullscreen-mode="fill-uniform" fill-auto-expand={true}
size={[1920, 1080]} background-color="linear-gradient(...)">
<basic-text position={[560, 24]} size={[800, 64]} text="标题"
text-font={{ size: 40, bold: true, color: 'linear-gradient(90deg,#7DE3FF,#2E8BFF)' }} />
</screen>
),
})
- pragma 两行不可省(
@jsx h/@jsxFrag Fragment):tsx 运行器按它选 JSX 工厂,缺了报React is not defined。 - 全文件只允许一条 import(runtime):live 的
page_apply_project有单 import 门禁;离线编译也照此约定。循环、常量、函数都写在文件内。
runtime API 全表
来自本技能运行时 scripts/vptsx/runtime.ts(源码即权威,签名如下):
| 导出 | 签名 / 用途 |
|---|---|
defineProject(spec) |
组装信封。spec: { name, screen, screens?, windows?, foregroundScreen?, backgroundScreen?, params?, settings?, connections?, connectionData?, dataRules? };screen/screens/windows 传 <screen>/<window> JSX 节点 |
h / Fragment |
JSX 工厂(pragma 指定);一般不手调 |
bind(conn, tbl, ...fields) |
字段绑定三元组:bind('c','t','A') → [['c','t','A']];bind('c','t','A','B') 多选 |
jsonConnection({ uid, name, table:{uid,name,fields}, rows }) |
内联静态数据连接(connectorType:"json",字段 uid=列名)。返回 { connection, connectionData }:connections: [x.connection]、connectionData: { ...x.connectionData }。⚠️ 仅本技能 runtime;live page_apply_project 服务端把 import 改写到平台自带 runtime,那里返回 { connection, data }(无 connectionData,展开得 {}→无行数据),live 下 connectionData 写字面量 { conn: [{ tableId, tableName, fields, rows }] } |
interactions(...groups) |
options.interactions 完整值:{ version:2, groups } |
on(trigger, ...actions) |
一组交互;trigger 为字符串或 { name, options }(触发器带参时用对象,如 on({ name:'timer', options:{ mode:'interval', interval:5 } }, ...)) |
act(name, options?) |
一个动作:act('switchScreen', { screenUID: [...] }) |
resetUids() |
重置自动 uid 计数(同进程编译多份工程时用) |
| 类型导出 | 节点/信封:VpNode VpPiece VpCommonProps VpScreenProps VpFont VpProjectEnvelope;绑定/连接:VpFieldTriple VpFieldValue VpConnectionField;交互:VpTrigger VpAction;piece 判别联合 VpScene3dPiece / VpMap2dPiece / VpScada2dPiece / VpKnownPiece / VpHostPieceMap / VpPieceNode / VpPieceOptionsMap(后 7 个转出自 types.generated) |
没有 helper 的连接器(mock/api/websocket/postMessage/csv…)直接写原始 JSON 对象进 connections(形态见 data.md),connectionData 手写一帧初始 rows:
const mockConn = {
uid: 'conn_kpi', name: '实时KPI(Mock)', type: 'mock', connectorType: 'mock', refreshInterval: 4,
config: { mock: { rowCount: 1, fields: [
{ name: '在线用户', alias: '在线用户', kind: 'wave', base: 1300, amplitude: 380, periodSec: 18, decimals: 0 },
] } },
tables: [{ uid: 'tbl_kpi', name: 'KPI', fields: [{ uid: '在线用户', alias: '在线用户', type: 'number' }] }],
}
// defineProject({ connections: [mockConn], connectionData: { conn_kpi: [{ tableId: 'tbl_kpi', rows: [{ 在线用户: 1280 }] }] }, ... })
JSON → TSX 机械映射规则(家族文档任何 JSON 片段都按此转写)
参考文档(widgets/ 各家族、data.md、interactions.md)里的持久化形态是 JSON;转成 TSX 是机械规则,无一例外:
| JSON 里的… | TSX 里写成… |
|---|---|
节点 "type": "chart-bar" |
标签名 <chart-bar …/>;页面/窗口是 <screen> / <window> |
options 里的每个键值 |
同名 kebab 属性:"bar-color": "#3aa1ff" → bar-color="#3aa1ff";非字符串用花括号 size={[420,300]}、show-legend={true}、text-font={{ size:20 }} |
options.interactions |
interactions={interactions(on(...))} 属性(helper 组装后仍落 options.interactions) |
widgets: [...](容器子组件) |
JSX children:<group-panel>…子组件…</group-panel> |
| 节点顶层字段(与 options 平级) | 同名保留 props:uid name(NODE_PROPS 成员)+ unitTemplate pieces conditionalStyles stateOverrides sizeModeOverrides states state enabled locked templateSubtypeLabel templateAlias masterId masterVersion props overrides master privateData extensions pending guideLines(后 20 个与 runtime NODE_PASSTHROUGH 一一对应),值直接写 JS 字面量 |
信封顶层 project.name/params/settings、connections、connectionData、dataRules |
defineProject({...}) 的同名字段(JSON 字面量原样放入) |
| 多页/窗口/前景背景层 | screens: [<screen/>…]、windows: [<window/>…]、foregroundScreen: <screen/> |
反向同理:page_export_tsx 导出的代码就是按这套规则从 JSON 再生的(属性值全是 JSON 字面量),读导出代码 = 读 JSON。
uid 纪律:省略 uid 自动生成 type-N(同类型递增)。想保留/对齐的组件、被引用的节点(交互 targetPath / dataRules / link-section / feature-panel-uid 等)必须显式写 uid。判定是 truthy 检查——uid=""/uid={0} 这类 falsy 值同样走 autoUid(不是 ?? 语义),别指望空串占位。
非平凡场景配方
1. data-repeater 的 unitTemplate(模板放顶层 prop,不是 children)
<data-repeater uid="w_rep" position={[80, 200]} size={[1760, 800]}
unit-size={[330, 170]} overflow="scroll-y" item-fields={bind('conn_x', 'tbl_x', '名称')}
unitTemplate={
<group-panel uid="w_rep__unit" position={[0, 0]} size={[330, 170]} background={true}>
<basic-text uid="tpl_name" position={[22, 18]} size={[286, 32]} value-field={bind('conn_x', 'tbl_x', '名称')} />
</group-panel>
} />
unitTemplate 值就是一个 JSX 节点(编译为 group-panel 节点对象);模板内子组件普通字段绑定自动取本行(行作用域见 widgets/containers.md §5b)。
2. component-instance(母版-实例,全在保留 props)
<component-instance uid="inst_kpi" masterId="master_kpi" masterVersion={1}
position={[300, 380]} size={[460, 320]}
props={{ title: '在线用户数', valueField: bind('conn_kpi', 'tbl_kpi', '在线用户') }}
overrides={{ m_title: { 'text-font': { size: 20, color: '#00FFC6' } } }}
master={masterDef} />
masterDef 是母版对象({ masterId, name, version, propsSchema, node },node 可用 JSX 变量),形态见 widgets/containers.md §5bb。
3. builder 型 pieces(map-2d / scada-2d / scene-3d):数组 prop + 类型标注
pieces 是节点顶层 prop(不是 children)。模块顶层构造 pieces 时标注判别联合类型,字面量才不拓宽、选项键与枚举获得编译期校验:
import { defineProject, h, type VpScene3dPiece } from '<...>/runtime'
const scenePieces: VpScene3dPiece[] = [
{ uid: 'scene3d-scene', type: 'scene3d-scene', options: { 'bg-mode': 'color', 'scene-bg': '#0a0f1c' } },
{ uid: 'scene3d-camera', type: 'scene3d-camera', options: { position: [16, 12, 16], target: [0, 1, 0] } },
{ uid: 'p_box', type: 'scene3d-shape', options: { shape: 'box', position: [0, 1, 0], 'data-key': 'PUMP' } },
]
// <scene-3d uid="w_scene" scene3d-initialized={true} pieces={scenePieces} … />
同理 VpMap2dPiece[](map-body 必须放第一个)、VpScada2dPiece[](图元用 geo-x/y/w/h)。内联写 pieces={[...]} 也可(types.generated 按宿主校验),配方与选项详见各家族文档。
4. 条件样式 / 多状态 / 设备差异(顶层 props + dataRules)
export default defineProject({
// 数据规则:JSON 字面量数组(形态见 data.md §dataRules)
dataRules: [{ uid: 'dc_alarm', name: '告警', rules: [[{ connection: 'conn_x', table: 'tbl_x', field: '状态', func: '=', args: '告警', method: 'some' }]] }],
screen: (
<screen uid="screen_main" layout-profile="canvas-scale" fullscreen-mode="fill-uniform" fill-auto-expand={true} size={[1920, 1080]}>
<basic-text uid="t_state" position={[100, 100]} size={[300, 60]}
text-font={{ size: 40, color: '#ffffff' }}
conditionalStyles={[{ paths: ['text-font'], cases: [
{ ruleId: 'dc_alarm', value: { size: 40, color: '#ff4d4f' } },
{ fallback: true, value: { size: 40, color: '#52c41a' } },
] }]} />
{/* 多状态:states 声明 + stateOverrides 按状态覆盖;设备差异 sizeModeOverrides 同理 */}
<basic-card uid="k1" position={[100, 200]} size={[220, 120]}
states={[{ uid: '' }, { uid: 's_warn', name: '警示' }]}
stateOverrides={{ s_warn: { 'text-font': { color: '#FFC940' } } }}
sizeModeOverrides={{ SMALL: { size: [160, 90] } }} />
</screen>
),
})
5. 多屏 + 窗口(画面复用)
export default defineProject({
name: '多页工程',
screen: mainScreen, // JSX 变量随意抽取
screens: [<screen uid="screen_2" …>…</screen>],
windows: [
<window uid="win_detail" name="设备详情" size={[560, 400]}>
<basic-text uid="wd_title" position={[24, 20]} size={[300, 36]} text="详情" />
</window>,
],
…
})
// 打开窗口:act('switchWindow', { windowUid: [{ targetPath: ['win_detail'] }], operate: 'open' })
// 页面内嵌复用:<window-instance window={[{ targetPath: ['win_detail'], refType: 'element' }]} content-fit="contain" … />
6. 循环 / 工厂消重复(TSX 的核心红利)
const KPIS = [
{ name: '总能耗', value: 12680, unit: 'kWh', color: '#00E5FF' },
{ name: '光伏出力', value: 4210, unit: 'kW', color: '#38BDF8' },
]
const kpiCard = (k: (typeof KPIS)[number], i: number) => (
<basic-card uid={`kpi_${i}`} position={[24 + i * 240, 120]} size={[220, 120]}
static-value={k.value} unit-text={k.unit} text-font={{ size: 34, bold: true, color: k.color }} />
)
// <screen …>{KPIS.map(kpiCard)}</screen>
工厂每次调用要产不同 uid;编译产物是展开后的多份独立节点。百分比自适应页把 geometry-unit="percent" 抽成常量对象展开:{...PCT}。
校验层次(谁兜什么)
| 层 | 兜住 | 说明 |
|---|---|---|
tsc(types.generated.ts) |
值类型 / select 枚举 / vector 元组长度 / 未知组件标签 | kebab 属性名被 TS 当 data-* 豁免多余属性检查,臆造名 tsc 不报 |
compile.mts 选项名检查 |
臆造/拼错的选项名(含 unitTemplate/pieces 递归) | 对照 widget-schema,报错带相近候选 |
validate-project-json.mjs |
绑定三元组可解析、连接一致性、position/size 缺失、map-body 等 | 与手写 JSON 同一终审 |
配对示例(学写法直接抄)
data/ 示例库的工程均有 .vp.tsx 与 .vpproj.json 成对:手写 TSX 源(map3d/map3dcomp,含循环/工厂/注释惯例)+ 机器导出 TSX(其余目录,从 JSON 生成并经回编译等值验证)。检索入口见 examples/catalog.md。
维护
- 权威链:
defineOptions()→widget-schema.generated.json→types.generated.ts/get_widget_metadata/page_get_schemas。本文档只教工作流与映射规则,不要把全量属性抄进文档。