唯杰地图 logo唯杰地图
VJMAP
EN

用 VP-TSX 环路生成 / 修改大屏页面

vjvp/references/page-live-loop.md 17.7 KB218 行
原文

把「用户的自然语言需求」变成 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,复用不靠模块导入,分三层:

  1. 代码级(写作期消重复):同一文件内用变量、工厂函数、map() 循环。工厂每次调用要产不同 uid;编译产物是展开后的多份独立节点。
  2. 画面级(运行时引用复用):画面定义一次放 defineProject({ windows: [<window uid="win_x">…</window>] })——弹窗/抽屉用交互动作 switchWindow 打开;页面内嵌复用用 window-instance 画面组件(options 的 window 引用窗口 uid + content-fit),同一画面可嵌任意多处、跨页面,改一处全部生效。
  3. 组件组级(母版-实例):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 从零生成

  1. 需求分析 → 在你的会话工作目录写好整页代码。
  2. page_apply_project({ path: "<该文件的绝对路径>" })——服务端自己读文件、编译、自适应写回。
  3. 报错就修 tsx 重试(错误信息带臆造选项名候选/uid 冲突位置);绝不退回 page_add_element 逐个加。
  4. page_screenshot 截图核对布局与配色;不符就回到 1 改代码再 apply。
  5. 确认后 page_save 持久化。

模式 C:live 修改已有页

  1. page_export_tsx({ path: "<会话目录绝对路径>/<名>.vp.tsx" })——当前页写到该文件(只回元数据省 token);多页工程只改某页就传 screenUid(主屏/子页面/前景层/背景层/窗口都可)导出单屏。
  2. 用文件工具读该文件,理解现状后按需求改写:改选项值、调布局、增删组件、换配色。不动的组件保留原 uid。
  3. page_apply_project({ path: "<同一路径>", screenUid: "<单屏时的目标屏>" })。
  4. 看返回结果(语义见下表)→ 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 的结果宣称完成。

质量杠杆(按影响排序,做到即高分)

  1. 截图核对循环:apply 后必 page_screenshot 对照需求,发现布局/配色/数据不符就回改代码再 apply——「编译通过」不等于「页面正确」。
  2. 预载陷阱:动手前扫 gotchas.md,可跳过绝大多数已知返工(黑边/换列/JSON 串键/单 import…)。
  3. uid 纪律:保留组件显式 uid、改动最小化——保 uid 的改动走增量而非整屏替换,历史与选中态都更稳。
  4. 组件选型对:按「组件映射」表选型,不要什么都用文本+图形拼。

交付前自检

[ ] 编译零 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 返回语义、截图核对结论);③ 需求点覆盖对照;④ 已知取舍与建议微调项。不要只说"已完成"。