用 VP-TSX 环路生成 / 修改大屏页面
把「用户的自然语言需求」变成 appvisual 大屏页面的代码化工作流:整页当作一份 *.vp.tsx 源码来写或改,编译成工程 JSON 后一次性落地。相比几十次细粒度 MCP 微操作:更快、更可靠、循环/变量消重复、类型与 schema 双重纠错。
心智模型:真源始终是页面 JSON;VP-TSX 是「每次导出即从当前页再生」的代码视图。人为改过页面后再导出,拿到的就是最新代码——不存在双向合并问题。
第一步:选模式
| 场景 | 模式 | 链路 |
|---|---|---|
| 没有打开的页面,要一份可导入的工程文件 | A 离线生成 | 写 .vp.tsx → compile.mts 编译(内置 schema 校验)→ 交付 .vpproj.json(编辑器首页「导入工程」) |
| 编辑器已打开(vp-page MCP 已连接),从零做新页 | B live 生成 | 写 .vp.tsx → page_apply_project({ path }) → 截图核对 → page_save |
| 编辑器已打开,要大改现有页面 | C live 修改 | page_export_tsx({ path }) → 用文件工具读改 → page_apply_project({ path }) → 核对 → page_save |
何时不用本环路:只改一两个已定位组件的个别属性/文字/颜色/数据行——直接 page_get_schemas → page_batch_set_options 定点修改最快。整页级别的生成、重排、批量增删、成套换色、或「先从代码看懂整页」才走环路。
live 模式前置条件:浏览器已打开编辑器工程且 vp-page MCP 会话已附着(工具报未附着时用 page_sessions 查看、必要时 page_attach 选定;工具名可能带服务前缀,如 user-page)。
第二步:需求分析(写代码前想清楚,所有模式通用)
按此清单把需求落成结构化决策,缺信息就先问用户:
[ ] 画布:固定 1920×1080 演示屏?任意窗口比例自适应?→ 决定 layout-profile + fullscreen-mode(三配方见下)
[ ] 风格:默认科技风深色;选一套配色 token(科技蓝/荧光青/深空紫/极光绿,见 [visual-style.md](visual-style.md))
[ ] 分区:顶栏(标题+时间) + 左/中/右栏或网格;每个分区放什么组件
[ ] 组件映射:KPI→basic-card、趋势→chart-line/area、占比→chart-pie、排行→basic-ranking-list/chart-bar(horizontal)、
明细→basic-table/basic-scroll-board、卡片墙→data-repeater、地图→map-2d、工艺图→scada-2d、三维场景/数字孪生→scene-3d
(80 个 type 的家族目录与关键 options 见 [widgets/index.md](widgets/index.md))
[ ] 数据:每个组件展示什么数据 → 设计 连接(conn)→表(tbl)→字段;静态演示用 jsonConnection 内联,
实时演示用 mock 连接器(refreshInterval + wave/walk 字段)
[ ] 交互:点击联动/切页/弹窗/轮询?→ interactions(on(触发器, act(动作)))
[ ] 动手前扫一遍 [gotchas.md](gotchas.md)(26 条陷阱,#21~#26 是 VP-TSX 专属)
铺满三配方(每个 <screen> 必写 layout-profile + fullscreen-mode,否则播放留黑边):
| 需求 | screen 属性 |
|---|---|
| 固定大屏(演示/LED) | layout-profile="canvas-scale" + fullscreen-mode="fill-uniform" + fill-auto-expand={true} + static 绝对坐标(不变形铺满;stretch 会变形只留对照,见 envelope.md §4.1) |
| 百分比自适应 | layout-profile="canvas-percent" + fullscreen-mode="responsive" + 顶层组件 geometry-unit="percent" |
| 流式自适应 | layout-profile="flow-fluid" + fullscreen-mode="responsive" + container-layout="fluid" + fluid-wrap="nowrap" |
第三步:写 VP-TSX
写法权威(runtime API 全表、JSON→TSX 机械映射规则、unitTemplate/pieces/条件样式/多屏窗口等非平凡配方):vptsx.md。文件骨架(模式 A 的完整头;live 模式 import 会被服务端自动归一化、路径写什么都行,但必须有且只有这一条 import):
⚠️ live 模式(B/C,
page_apply_project)编译用的是平台自带 runtime,不是本技能的scripts/vptsx/runtime.ts:服务端把这条 import 改写指向平台自带的 runtime(发布包同源拷贝),其jsonConnection返回{ connection, data }——没有connectionData键,{ ...sales.connectionData }在 live 下展开成{},页面载入无行数据(校验只给 WARN)。故connectionData一律写字面量(下例写法两种模式都对);jsonConnection(...).connectionData只在本技能离线compile.mts下可用。另一条路:page_apply_project({ path })也直接吃.vpproj.json/.json(不编译、原样应用),可先用本技能compile.mts离线编出 JSON 再 apply,完全绕开平台 runtime 差异。
/** @jsx h */
/** @jsxFrag Fragment */
import { bind, defineProject, h, jsonConnection } from '../vjvp/scripts/vptsx/runtime'
const salesFields = [
{ uid: '月份', alias: '月份', type: 'string' },
{ uid: '销售额', alias: '销售额', type: 'number' },
]
const salesRows = [{ 月份: '一月', 销售额: 120 }, { 月份: '二月', 销售额: 200 }]
const sales = jsonConnection({
uid: 'conn_sales', name: '销售数据',
table: { uid: 'tbl_m', name: '月度', fields: salesFields },
rows: salesRows,
})
export default defineProject({
name: '销售监控大屏',
connections: [sales.connection],
// 字面量:live(平台 runtime 无 .connectionData)与离线编译都正确
connectionData: { conn_sales: [{ tableId: 'tbl_m', tableName: '月度', fields: salesFields, rows: salesRows }] },
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(180deg,#020A18 0%,#081A38 55%,#04101F 100%)">
<basic-text uid="title" position={[560, 24]} size={[800, 64]} text="销售监控"
text-font={{ size: 40, bold: true, color: 'linear-gradient(90deg,#7DE3FF,#2E8BFF)' }} />
<group-panel uid="panel_trend" position={[24, 120]} size={[900, 520]} background={true}>
<chart-bar uid="bar_month" position={[20, 56]} size={[860, 440]}
category-field={bind('conn_sales', 'tbl_m', '月份')}
metric-fields={bind('conn_sales', 'tbl_m', '销售额')} />
</group-panel>
</screen>
),
})
写法规则(错一条就编译失败或产出坏页面):
- pragma 两行不可省(
@jsx h/@jsxFrag Fragment),缺了报React is not defined。 - 全文件只允许一条 import:live 的
page_apply_project有单 import 门禁(编译是执行式的,多余 import / 动态import()/require()一律被拒绝)。循环、常量、函数都写在文件内。 - 标签名 = widget type(
<basic-card>、<chart-bar>、<group-panel>…);<screen>/<window>是页面/窗口。 - 属性名 = schema 的 kebab-case 原名,与
page_get_schemas/get_widget_metadata完全同名。绝不臆造(font-color/font-size/text-align多半是错的;文字颜色在text-font.color,背景是background-color)。拿不准就先查 schema。 - 字段绑定用
bind(conn, tbl, 字段)→[[conn,tbl,字段]]三元组;引用的 uid 必须与connections/connectionData逐字一致。 - 事件交互:
interactions={interactions(on('click', act('switchScreen', {...})))}(触发器/动作名见 interactions.md)。 - uid 纪律:
- 想保留/对齐的组件必须显式写 uid(apply 按 uid 对齐;保 uid 的改动更可能走增量而非整屏替换);
- 被引用的节点(交互 targetPath / 数据规则 /
link-section等)必须显式 uid; - 省略 uid 会自动生成
type-N,重复 apply 时可能与已有 uid 撞车(校验器会报uid 重复,报了就补显式 uid)。
- 静态布局下每个组件必写
position+size(流式布局子组件改用fluid-*选项)。 - 复杂子结构走节点保留 props(与 options 平级、原样落节点顶层,完整清单):
unitTemplate(data-repeater 模板)/pieces(map-2d、scada-2d、scene-3d 图元)/conditionalStyles/stateOverrides/sizeModeOverrides/states/state/enabled/locked/templateSubtypeLabel/templateAlias/masterId/masterVersion/props/overrides/master/privateData(私有数据)/extensions(二开脚本)/pending(页面暂存区)/guideLines(参考线)。这些字段环路全程无损往返,任一变化按整屏替换应用。
画面 / 窗口 / 组件组的复用(不是 import)
VP-TSX 只允许一条 import,复用不靠模块导入,分三层:
- 代码级(写作期消重复):同一文件内用变量、工厂函数、
map()循环。工厂每次调用要产不同 uid;编译产物是展开后的多份独立节点。 - 画面级(运行时引用复用):画面定义一次放
defineProject({ windows: [<window uid="win_x">…</window>] })——弹窗/抽屉用交互动作switchWindow打开;页面内嵌复用用window-instance画面组件(options 的window引用窗口 uid +content-fit),同一画面可嵌任意多处、跨页面,改一处全部生效。 - 组件组级(母版-实例):
component-instance组合组件——节点顶层写masterId+props(实例传参)+overrides(覆盖组内子组件选项)+master(内嵌母版),一份母版派生多实例:
<component-instance uid="kpi_cpu" masterId="master_kpi" position={[100, 100]} size={[220, 120]}
props={{ title: 'CPU 使用率', unit: '%', valueField: bind('conn_kpi', 'tbl_kpi', 'CPU') }}
overrides={{ m_title: { 'text-font': { size: 16, color: '#FFC940' } } }}
master={master_kpi_def} />
含画面/窗口复用工程的环路注意:
- 单屏导出(
screenUid)不带windows定义——live 环路没问题(apply 按 uid 合并回整工程、窗口仍在),但离线交付单屏文件会让window-instance/switchWindow引用悬空,此类工程请导出整工程。 - 增删窗口本身要用整工程 export→apply(单屏合并不改 windows 集合)。
组件选项、数据源、交互、视觉规范的全部细节按任务查 project-index.md + widgets/index.md(任务形总路由:组件家族目录 / 科技风 token / 数据绑定 / 示例目录),离线产出链与校验层次见 vptsx.md。
模式 A:离线生成可导入工程
工作文件放 <工作区根>/.vptsx-live/(已 gitignore)或用户指定目录;从 .vptsx-live/ 出发 runtime 导入路径就是骨架里那条 ../vjvp/scripts/vptsx/runtime。
# 1.(可选)单文件类型检查——值/枚举/元组错误带行号报出(单行执行,PowerShell/bash 通用)
pnpm exec tsc --noEmit --strict --skipLibCheck --jsx react --jsxFactory h --jsxFragmentFactory Fragment --target ES2022 --module ESNext --moduleResolution Bundler .vptsx-live/<名>.vp.tsx
# 2. 编译(执行 + 臆造选项名拦截 + validate-project-json 终审,一条命令全包;省略 -o 则写到输入同目录)
pnpm exec tsx vjvp/scripts/vptsx/compile.mts .vptsx-live/<名>.vp.tsx -o <交付路径>.vpproj.json
有 ERROR 必须修掉重编译;产物 .vpproj.json 交付给用户,在编辑器首页「导入工程」即可使用。
模式 B:live 从零生成
- 需求分析 → 在你的会话工作目录写好整页代码。
page_apply_project({ path: "<该文件的绝对路径>" })——服务端自己读文件、编译、自适应写回。- 报错就修 tsx 重试(错误信息带臆造选项名候选/uid 冲突位置);绝不退回
page_add_element逐个加。 page_screenshot截图核对布局与配色;不符就回到 1 改代码再 apply。- 确认后
page_save持久化。
模式 C:live 修改已有页
page_export_tsx({ path: "<会话目录绝对路径>/<名>.vp.tsx" })——当前页写到该文件(只回元数据省 token);多页工程只改某页就传screenUid(主屏/子页面/前景层/背景层/窗口都可)导出单屏。- 用文件工具读该文件,理解现状后按需求改写:改选项值、调布局、增删组件、换配色。不动的组件保留原 uid。
page_apply_project({ path: "<同一路径>", screenUid: "<单屏时的目标屏>" })。- 看返回结果(语义见下表)→
page_screenshot或page_get_elements核对 →page_save。
应用语义(看懂 apply 的返回,别误报)
page_apply_project 先与当前页 diff,再自适应选择应用方式:
| 返回 | 含义 | 你该做什么 |
|---|---|---|
mode:"incremental", changed:true |
小改走增量:仅选项值/数据行/参数/工程名/组件名变化(选项与改名合并为一步可撤销;数据/参数不进撤销栈) | 核对后保存 |
mode:"replace", changed:true, historyReset:true |
大改整屏替换:结构变化(增删/改序/改父)、复杂子结构、连接定义、屏级选项、删除选项键、增量条目超阈值(60) | 结果不对就立即 page_history(action=undo) 一次性还原替换前页面(做任何其它修改后快照失效);对了就保存 |
changed:false, applied:false |
与当前页无差异,什么都没改 | 检查是不是改错了文件/忘了保存编辑;不要声称已完成 |
| 报错(编译/校验失败) | 臆造选项名、uid 缺失/重复、结构非法 | 按错误信息修 tsx 重试 |
关键语义:
- 单屏合并:输入只含一个
screen(无 screens/windows/前景/背景层)时按 uid 合并进当前整工程,其它页保留;连接是并集(增改不删——要删数据源需导出整工程改完整体应用),dataRules 仅在非空时整体覆盖。 - screenUid:单屏输入=合并目标;整工程输入时只收窄 diff 判定范围,一旦触发替换仍替换整个工程。
- 想恢复某选项默认值:显式写出默认值,不要删那一行(删键触发整屏替换)。
- 工程名:tsx 里的
name:参与应用(缺省/空=不改名)。
铁律
- path-first:同机一律传文件绝对路径,别把整段 tsx 内联进参数(费 token);
tsx/envelope参数只在跨机无共享盘时用。 - 产物位置:只写自己会话工作目录或平台根下的
.vptsx-live/。服务端强制拒绝把导出写进平台目录内其它路径(防覆盖源码),别尝试。 - 失败修代码重试,禁止退回细粒度逐个加组件。
- 属性名唯一权威是 schema(
page_get_schemas/get_widget_metadata);文档里的属性简称不能直接当 path 用。三维场景用get_doc({ type: "scene-3d" })+ 各 piece type 的 metadata,不要用 vjmap-docs 猜pieces选项名。
失败恢复(有界重试,禁止逃逸)
失败分类 → 修复动作:
| 失败形态 | 修复动作 |
|---|---|
| tsc 类型检查报错(值/枚举/元组) | 按行号改值类型;类型来自 types.generated.ts,不要用 as any 压掉 |
| 编译/apply 报臆造选项名(带候选提示) | 按候选改成 schema 原名;拿不准先 page_get_schemas 再改 |
| apply 报 uid 缺失/重复 | 给被引用/要保留的组件补显式 uid;重复的改名 |
| 校验器 ERROR(绑定解析失败/结构非法) | 逐字核对三元组与 connections/connectionData;结构对照 envelope.md |
changed:false, applied:false |
不是成功——排查是否改错文件/忘保存编辑/内容与当前页本来相同;不得声称已完成 |
| 替换后页面不对 | 立即 page_history(action=undo) 一次性还原(做任何其它修改后快照失效),再改代码重 apply |
纪律:
- 一轮修完全部报错再重试(报错信息通常一次给全,不要修一条试一次)。
- 同一问题最多 3 轮仍失败:停下,把最新报错原文、当前 tsx 关键片段与已尝试的修复如实报告用户。
- 反逃逸:禁止退回
page_add_element逐个加组件;禁止删需求功能绕过报错;禁止砍掉绑定改成静态数据糊弄;禁止对changed:false或未page_save的结果宣称完成。
质量杠杆(按影响排序,做到即高分)
- 截图核对循环:apply 后必
page_screenshot对照需求,发现布局/配色/数据不符就回改代码再 apply——「编译通过」不等于「页面正确」。 - 预载陷阱:动手前扫 gotchas.md,可跳过绝大多数已知返工(黑边/换列/JSON 串键/单 import…)。
- uid 纪律:保留组件显式 uid、改动最小化——保 uid 的改动走增量而非整屏替换,历史与选中态都更稳。
- 组件选型对:按「组件映射」表选型,不要什么都用文本+图形拼。
交付前自检
[ ] 编译零 ERROR(模式 A:compile.mts + 校验器;live:apply 无报错)
[ ] 每个 screen 写了 layout-profile + fullscreen-mode;静态布局组件都有 position/size
[ ] 字段绑定三元组的 conn/tbl/字段 uid 三者都存在且逐字一致
[ ] 保留组件的 uid 未变;新增组件 uid 全局唯一
[ ] live:apply 返回 changed:true 且截图/get_elements 与需求相符;changed:false 时已排查原因
[ ] live:确认无误后已 page_save(运行态/未保存的修改刷新即丢)
交付时报告
向用户交付时给出:① 模式与产物(A:.vpproj.json 路径;B/C:已 apply+save 的页面);② 验证证据(编译/校验结果、apply 返回语义、截图核对结论);③ 需求点覆盖对照;④ 已知取舍与建议微调项。不要只说"已完成"。