# SDK 使用（vjvpsdk：嵌入、程序化操作、脚本、自定义组件）

> **包名**：对外发布的 npm 包名是 **`vjvpsdk`**（CSS 子路径 `vjvpsdk/vp-runtime.css`）。外部工程一律 `import ... from 'vjvpsdk'`（例外：编辑器导出的「二次开发工程包」不依赖 vjvpsdk，`'@vp/sdk'` 由 vite 别名指向随包 `vendor/vp-runtime/vp-runtime.mjs`，见 [deploy-and-integrate.md](deploy-and-integrate.md) §3）。

## 1. 挂载

```ts
import { createProject, createVpViewer, type VpProjectBody } from 'vjvpsdk'

const body = createProject({
  name: '实时看板',
  subtitle: 'createProject 构造可挂载的工程 body',
  widgets: [/* VpNode */],
  connections: [/* ... */],
  connectionData: { /* ... */ },
})

const ctx = createVpViewer('#app', body /* 或编辑器导出的 {project,connections,connectionData,dataRules} */, {
  params: { theme: 'dark' },            // 项目参数（千人千面），与 project.params 合并
  transformData(body) { /* 挂载前加工工程数据（返回新 body 或就地改）。编辑器「导出工程」的 .vpproj.json 是精简形态，
                           SDK 不补齐 connectionData 的 tableName/fields → 在此从 connections 补齐，写法见 SKILL.md 骨架 */ },
  widgets: [{ type: 'my-gauge', module }], // 自定义组件（见 §6）
  carousel: false,                       // 多页自动轮播
  onReady(ctx) { /* 就绪，程序化操作 */ },
  onBehavior(event, payload, ctx) {},    // 监听全部行为事件
  onScript(command, ctx) {},             // 接管「执行脚本」动作（不再 POST /execute-script）
  onRequest(req, ctx) { return data },   // 接管「调用接口」动作（加签/代理/Mock；返回解析后数据）。
                                        // 栈式接管：后注册者优先，退订后回落前一个；无人接管回落默认 fetch
})
```

内部与编辑器预览/独立 viewer 完全同源：注册内置组件 → 解析数据 → 构建工程模型置播放态 → 渲染舞台 → 启动取数/启动窗口/交互。

## 2. 运行上下文 ctx（VpViewerContext）

| 分类 | API |
|---|---|
| 页面 | `screens` / `activeScreen` / `getScreen(uid?)` / `setActiveScreen(uid)` / `addScreen(input)` / `removeScreen(uid)` |
| 窗口 | `windows` / `openWindows` / `getWindow` / `openWindow(uid, context?)` / `closeWindow` / `closeTopWindow` / `addWindow` / `removeWindow` |
| 组件 | `getWidget(uid)` / `findWidgets(pred)` / `getWidgetsByType(type)`（覆盖页面+窗口）/ `addWidget(type|input, {screenId?})` / `removeWidget(uid)` / `getMap(uid)` / `getScada(uid)` |
| 数据 | `setData(conn, table, rows)` 推数即重渲（rows **原样整表替换**：键须是字段 uid，**不走 `sourceField` 映射**；连接或表不存在时静默无操作。对比：`startEventInbound` 的 `setData`、`requestApi` 写数据源、连接器取数会按 `sourceField` 映射并丢弃未声明列）/ `getData` / `getConnections()`（`VpConnectionInfo[]`，`uid` 字段）/ `getTables(conn)`（`VpTableInfo[]`，**`tableId`/`tableName`/`rowCount` 字段，无 `uid`**）/ `refreshData(conn?)` / `onDataChange(conn, table, fn)` |
| 参数 | `getParams()` / `setParams({...})`（驱动数据规则/条件样式/状态重算）/ `onParamsChange(fn)` |
| 事件 | `on(name?, handler)` 行为事件总线——**只收组件「发送行为事件」动作派发的事件**（`sendBehaviorEvent` → 宿主；该动作**没有 data 选项**，宿主收到的 payload 是本条交互的触发器 payload：`click` 为 `undefined`、`itemClicked` 为 `{别名:值}`、`click-row` 为行 payload；动作的 `element` 解析不到时直接返回，宿主也收不到）/ `emit(name, data?, targetUid?)` 投递**进**工程驱动「接收二开事件」触发器，**不会回 `on` 总线**（方向相反，勿用 emit+on 自测回路）/ `startEventInbound(source, opts)` 第三方入站 / `onScript` / `onRequest` |
| 生命周期 | `reload(body)` / `destroy()`；逃生舱 `ctx.project`（内核 VpProject，方法面与扩展脚本门面**不同名**：用 `ctx.project.getElementByUID(['w_bar'])`（**参数是路径数组** `[screenUid?, widgetUid, pieceUid?]`，传裸字符串会被按字符拆开、静默返回 `undefined`）或 `ctx.project.findWidgetByUid('w_bar')`，而非 `getElement`） |

> ⚠️ **byId 族只在 `ctx.project`（内核 VpProject）上，viewer/ctx 上没有**：`getWindows()` / `openWindowById(uid)` / `closeWindowById(uid)` / `setActiveScreenById(id)` 是 VpProject 的方法，在 viewer 上调同名得到 `undefined`。它们与 viewer 的 `openWindow(uid)` / `setActiveScreen(uid)` 并存且参数同为 uid（返回的窗口项 uid 形如 `window_xxx`，自建窗口用传入 uid）。widget 句柄侧：`setEnabled(b)` 返回句柄可链式、`onClick(fn)` 返回退订、`setPrivateData/getPrivateData` 存取 `fields/rows` 载荷；`getExtensionInstances` 属扩展内 `this.element`（VpExtensionElementApi），**不在** widget 句柄。

> ⚠️ **`destroy()` 拔全局事件通道**：宿主行为事件经模块级单例 `setBehaviorObserver` 派发——`createVpViewer` 覆盖它、`destroy()` 置 `null`（`sdk/index.ts`）。多 viewer 共存时**只有最后创建的实例 `on()` 能收到**；任一实例 destroy 后所有存活 viewer 的 `ctx.on` 永久失联（`sendBehaviorEvent` 动作本身仍正常写 `status.receiveBehaviorEvent`，只是不再回传宿主）。同页多实例/动态增删 viewer 场景须重建或自接 observer。

常见任务：

```ts
// 实时推数（绑定该表的所有组件自动刷新）
setInterval(() => ctx.setData('conn-sales', 'monthly', rows), 3000)
// 监听大屏「发送行为事件」动作
ctx.on('open-detail', (event, payload) => { ... })
// 程序化建组件并绑字段
const bar = await ctx.addWidget({ type: 'chart-bar', name: '月度销售', position: [40,120], size: [780,360] })
bar?.addField(['conn-sales', 'monthly', 'month'])   // 自动路由到最合适字段槽
bar?.addField(['conn-sales', 'monthly', 'sales'])
```

## 3. 组件句柄 VpWidgetHandle（getWidget 返回，链式）

- 属性：`getOption/setOption(path, v)` / `setOptions({..})` / `getOptions()`；几何 `getPosition()` 返回 **`{left, top}` 对象**（非数组），`setPosition` 收 `{left,top}` 或 `[x,y]`；`get/setSize`、`setRotate`。**注意** `setRotate` 只是 `setOption('rotate', [x,y,z])`——`getRotate()` 读的是 `widget.rotate` 字段，对未声明 rotate 选项的组件（如 chart-bar）二者不同步（option 写入但 `getRotate` 恒 `{x:0,y:0,z:0}`）。
- **`path` 语义**：字符串是**字面 key**——`setOption('title.text', v)` 写的是 `options['title.text']` 扁平键（VP 选项本就全是扁平连字符名如 `'category-field'`）；要真嵌套写须传**数组** `setOption(['a','b'], v)`。`setOptions` 是逐 key 合并，不会清空其它选项。
- 显隐命名：`isVisible/setVisible/show/hide/setEnabled/setName`。注意 `setName` 写的是组件节点字段、不是 `options.name`——`getOption('name')` 读不到它。
- 数据：`getData()`（读数 facade）/ `getBoundFields(path)` / `bindField(path, [c,t,f])`（显式绑）/ `addField([c,t,f])`（按推荐类型自动路由）/ `setData(rows)`（推给首个绑定表，同样原样替换不映射；未绑定则写私有表）/ `get/setPrivateData`。
- 选中与值：`getValue/getSelectedData/select(index, entry)`。
- 树寻址：`getChildren()/addChild(input)/getParent()/getScreen()`；专用句柄 `asMap()` / `asScada()`。
- builder 子元素（三维/组态/地图 pieces）：`getPieces()` / `addPiece({type,uid?,name?,options?})` / `removePiece(uid)`。引擎已创建时新增会自动 init；`scene3d-scene` / `scene3d-camera` / `map-body` 拒绝删除。不要只改 `raw.node.pieces`。
- 事件订阅（都返回退订函数）：`on(event, fn)` / `onClick(fn)` / `onSelect(fn)`（点选/取消）/ `onValueChange(fn)`（控件值）/ `watchOption(path, fn)`；程序化 `click()` / `emit(name, data)`。
- 脚本：`getExtensions()/addExtension({name, code})/updateExtension(uid, patch)/removeExtension(uid)`。
- `remove()` 从工程移除。

页面句柄 `VpScreenHandle`：`getWidgets/getAllWidgets/getWidget/findWidgets/getWidgetsByType/addWidget/removeWidget/activate()/getSize/onAppear/get/setOption`。窗口句柄 `VpWindowHandle`：同页面的组件操作 + `isOpen/open(context?)/close()/onOpen`。

## 4. 地图句柄 VpMapHandle（`ctx.getMap(uid)` / `handle.asMap()`）

- 图层：`listLayers()`（uid/type/name/enabled/visible）/ `getLayer(uid)` → 句柄 `setOption/setOptions/setVisible/show/hide`；快捷 `setLayerOption/setLayerVisible`。`listLayers()` 不含 `map-body`（本体不是图层），但 `getLayer("map-body")` 能取到本体句柄；`listSources()` 每项含 `state`（`ready`）与 `recordCount`。
- 数据源：`listSources()` / `getSource(uid)` → `setData(geojson|rows|文本)`（运行态注入，不持久化）/`refresh()`/`setVisible`；快捷 `setSourceData/setSourceVisible/refreshData()`。
- 相机：`flyTo({lng,lat,zoom,bearing,pitch}, {duration})` / `fitData()` / `fitWhole()` / `setZoom(z)` / `clearSelection()`。
- **交互绘制/图形编辑**：`canDraw()` 判可用（地图异步初始化，建议 `map-ready` 后再问）→ `startDraw(init?)` 开会话 → `getDraw()` 取当前会话。
  会话 `VpMapDrawSession` 包的是引擎 vjmap `Draw.Tool`，交互都是引擎原生的：
  双击/回车结束绘制、点中图形拖顶点、点边中点加顶点、Backspace 删顶点、Esc 取消。
  - `init`：`mode`(select/edit/browse/point/line/polygon/rectangle/circle) / `features`(初始几何，用于**编辑既有图形**) /
    `toolbar`(false=不建引擎按钮，自己做界面；数组=只留这几个按钮) / `toolbarPosition` / `snap` / `guides` / `keybindings`。
  - 会话：`setMode(mode, {featureId?})`（`edit` 需 featureId，缺省取当前选中，没有则降级 select）/ `getMode()` /
    `set(features)` / `add(feature)` / `getAll()` / `getSelectedIds()` / `delete(ids)` / `deleteAll()` / `trash()` /
    `undo()` / `redo()` / `setFeatureProperty(id, prop, value)` / `onChange(fn)` / `dispose()`。
  - `onChange` 把引擎的 create/update/delete/selectionchange/modechange 归一为一路回调：
    `{ kind, features, action?, all, mode }`，`all` 是变化后的全量要素（省一次 getAll）。
  - 几何一律是**地图渲染坐标**（地理底图=经纬度）；CAD 底图要源 X/Y 时用 `handle.raw.map.fromLngLat` 自行换算。
  - 同一地图重复 `startDraw` 返回同一会话（避免叠出多个控件），要换初始化参数先 `dispose()`。

- **原生 `vjmap.Map`（逃生舱）**：`VpMapHandle` 没有单独的 `getNativeMap()`。`handle.raw` 是 `map-2d` 内核实例，就绪后：
  - `raw.map` → `vjmap.Map`（未就绪为 `null`）
  - `raw.vjmapModule` → 组件已加载的 `vjmap` 命名空间（用这份写 `Marker`/`Popup`/`Draw`，不要再 `import('vjmap')` 出第二份运行时）
  - `raw.isMapReady` → 首帧加载完成
  地图是异步 `createMap` 的，`createVpViewer` / `onReady` 时 `raw.map` 往往还是 `null`，先等到 `isMapReady`。（CAD blank 模式下 `raw.map` 即刻非 null 且 `fromLngLat` 可调用，但 `isMapReady` 仍为 false——`raw.map` 存在 ≠ 地图就绪，坐标换算可用、瓦片/样式操作仍要等 `isMapReady`。）可写的能力（自定义图层、查询、临时 Marker）用原生 SDK；图层显隐/推数/飞镜/绘制仍走句柄。不要 `raw.map.remove()` / 整表 `setStyle()`。
- **原生 `vjmap3d.App`**：三维没有独立 handle。`ctx.getWidget(uid).raw` 就绪后：
  - `raw.app` → `vjmap3d.App`
  - `raw.engineModule` / `raw.threeModule` → 已加载的 vjmap3d / three
  - `raw.appReady` → App 创建完成
  调用引擎 API 前先 `engineModule.Store.app = app`（多实例会抢全局指针）。不要 `app.dispose()`。

## 5. 组态句柄 VpScadaHandle（`ctx.getScada(uid)` / `handle.asScada()`）

- 测点：`setPoint(key, v)` / `setPoints({..})`（运行层合并写入，覆盖绑定值）/ `getPoint(key)` / `clearPoints()` / `pointKeys()`。
- 枚举：`listNodes()` / `listLinks()`。
- 改图元选项：`setPieceOption(pieceUid, option, value)`（运行时、不记历史；典型：`sprite-stop-frame` 指定图像集第几张、`sprite-play` 起停。找不到目标件返回 `false`）。
- 播放态点击订阅：`onNodeClick/onNodeDblClick(fn(payload{nodeUid,shape,dataKey,value}))` / `onLinkClick(fn)` / `onBlankClick(fn)`——四个均返回退订函数。注意命中检测依赖播放态渲染管线，纯 `dispatchEvent` 合成点击不一定触发回调（空数组），真实环境以用户交互为准。

另：widget 模型层 `raw.getData()` 返回 `VpDataReader`（`getRows/getColumns/getPagingRows`）——`getColumns([])`/`afterLinkageAndFilterRows([])` 在无绑定时返回 `null`，`getPagingRows({page,size})` 返回 `{rows:[],count:0}`；`VpWidget.defineOptions()` 静态返回选项蓝图分组数组（basic-text 为 3 组）。

## 6. 自定义组件

组件模块 `VpWidgetModule` = `TheWidget`(模型类,继承 VpWidget,静态 `defineOptions()` 声明选项蓝图；简单展示件可省) + `component`(Vue 渲染) + `meta`(type/name/category/defaultSize/defaultOptions)。注册二选一：挂载选项 `widgets: [{type, module}]`，或挂载前 `registerWidgetType(type, module)`。注册后该 type 可被工程 JSON 引用、`ctx.addWidget(type)` 创建、参与导出。⚠️ `meta.defaultOptions` 只在 `ctx.addWidget`/编辑器插入时补缺；从工程 JSON 载入的节点只用 `defineOptions()` 蓝图默认——载入期就要生效的默认值写进蓝图 `default`。

开发所需 API **均由 `vjvpsdk` 直接导出**（二开工程包里经 `@vp/sdk` 别名引用，同一套导出）：`VpWidget` 基类、`useWidget()/useWidgetSafe()`（Vue 组件内取模型实例，读 `getOption`/`getBoundFields`/`afterLinkageAndFilterRows`）、类型 `VpOptionGroupDef`/`VpOptionItemDef`。`defineOptions()` 写法与 `field(...)` 绑定约束详见官方文档的自定义组件章节。

另：`ctx.addWidget` / `screen.addWidget` 的入参支持 `pieces`（builder 型组件），可程序化创建带完整场景的 `scene-3d` / `scada-2d` / `map-2d`。

## 7. 二次开发脚本（代码面板 Extension）

随工程持久化（节点 `extensions`），运行态自动实例化（编辑态不执行）。形态：`export default class` + 生命周期 `created/mounted/ready/update(每帧)/destroy/onEnable/onDisable/onPropertyChanged`。API：

```js
this.element.getOption/setOption/show/hide/addEventListener('click',fn)/emit/getData/getChildren/getParent/getExtensionInstance
this.project.getElement(uid)/getElementsByType/getData/setData/setParams/refreshData/emit(name,data,targetUid?)
```

与交互「执行脚本」动作互补：动作派发命令串由宿主 `ctx.onScript` 接管（不接管走后端 `/execute-script`）；脚本是常驻逻辑。SDK 侧可用组件句柄增删改脚本（§3）。

## 8. 事件入站（第三方系统 → 大屏）

`ctx.startEventInbound(source, options)`：内置源工厂 `postMessageEventSource` / `websocketEventSource` / `pollingEventSource` / `pocketbaseEventSource`（实时订阅）。选项含解析/白名单 `allowNames`/去重 `dedupBy`/限流 `minIntervalMs`，落点默认 `emit` 行为事件，也可 `setData:{connection, table, mode:replace|append|prepend}` 写数据源，二者可并存。返回退订函数（destroy 自动停止）。

`ctx.emit(name, data?, targetUid?)` 定向语义：`targetUid` 命中组件/页面/窗口则**只投该元素**的 `status.receiveBehaviorEvent`；缺省**广播**全部页面+窗口+组件（同一 payload 引用）。

> ⚠️ **`emit` 落点语义**：入站 emit 走的是 `emitBehaviorEvent`——把 `{name,data}` 写进每个组件/页面的 `status.receiveBehaviorEvent`（驱动组件上配置的「接收二次开发事件」触发器），**不是**直接派发给 `ctx.on(name)`。`ctx.on` 收的是反方向的：组件交互里配「发送行为事件」动作 → 派给宿主。要观察入站落地可直接读 `handle.raw.status.receiveBehaviorEvent`；postMessage 消息形态 `{type:'vp-event', name, data}`（`parseInboundEvent` 也兼容 `{name|event|eventName, data|payload}`）。

## 9. 版本与产物

- 推荐：直接 `import 'vjvpsdk'` 解析编译产物。
- 业务应用不要给 SDK 配置指向源码的 Vite 别名。
- 对外产物：`pnpm package:sdk` / 二开工程包 `vendor/vp-runtime/`（`.mjs`+`.css`+`.d.ts`），使用时 `import '../vendor/vp-runtime/vp-runtime.css'`。
