

# 用 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](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](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 差异。

```tsx
/** @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](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`（内嵌母版），一份母版派生多实例：

```tsx
<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](project-index.md) + [widgets/index.md](widgets/index.md)（任务形总路由：组件家族目录 / 科技风 token / 数据绑定 / 示例目录），离线产出链与校验层次见 [vptsx.md](vptsx.md)。

## 模式 A：离线生成可导入工程

工作文件放 `<工作区根>/.vptsx-live/`（已 gitignore）或用户指定目录；从 `.vptsx-live/` 出发 runtime 导入路径就是骨架里那条 `../vjvp/scripts/vptsx/runtime`。

```bash
# 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](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 返回语义、截图核对结论）；③ 需求点覆盖对照；④ 已知取舍与建议微调项。不要只说"已完成"。
