# 数据子系统：connections / connectionData / dataRules

工程的数据分三块（均为 body 顶层）：

- `connections[]`：数据源**定义**（连接 + 表 + 字段，**不含行数据**）。
- `connectionData{}`：每个连接的**行数据**（`connUid → 表桶[]`）。
- `dataRules[]`：可复用的**数据规则**（联动/筛选/状态切换用）。

组件通过三元组 `[connUid, tableUid, fieldUid]` 绑定到某连接某表某字段：字段**元信息**(别名/类型)从 `connections[].tables[].fields` 取，字段**值**从 `connectionData` 的 `rows` 取。

**精简交换格式（仅限编辑器导入；手写给 SDK 挂载默认用完整写法）**：`connections[].tables[]` 是表名/字段的**单一真源**；`connectionData` 可只写 `tableId + rows`，其 `tableName/fields` 可省略——导入编辑器时会自动从 `connections` 对应表补齐为完整内部结构。仅当某表的数据字段**确需不同于**连接定义（如单独改过 `revisedType`/别名）时，才在 `connectionData` 显式重复写 `fields`。`tableId` 与 `rows` 永远要写。

> ⚠️ **精简写法只对「编辑器导入」安全**：补齐发生在编辑器导入路径；**SDK `createVpViewer` / `ctx.reload` 直挂不补齐**——`VpElement.getFieldMeta` 读 `connData` 的 `fields.find`（源码缺 `?.`），精简写法会让**挂载同步抛 `Cannot read properties of undefined (reading 'find')`**。SDK 场景必须写全 `tableName + fields`，或照抄 `sdk-demo` 的 `mergeFieldSchema` 先合并再挂载（见 [sdk-app/scaffold.md](sdk-app/scaffold.md) §3）。**编辑器「导出工程」的 `.vpproj.json` 本身就是精简形态**（`compactProjectBody`；二次开发工程包内的 `src/data/project.json` 才是完整 body），拿它喂 SDK 须在 `createVpViewer` 的 `transformData` 里从 `connections` 补齐（SKILL.md 骨架已带）。

## 1. connection 对象

```ts
interface VpConnection {
  uid: string                 // conn_xxx
  name: string
  type: string                // 旧字段：static / file / api ...（必填）
  category?: 'platform' | 'http' | 'local' | 'file' | 'db'
  connectorType?: string      // 具体连接器：mock/json/csv/excel/api/websocket/webhook/postMessage
  config?: Record<string, unknown>
  refreshInterval?: number    // 秒，0=不刷新
  tables?: VpTable[]          // 表（含字段，无行）
}
interface VpTable { uid: string; name: string; fields: VpField[]; eventName?: string; eventParams?: string; source?: string /* 外部源标识；pocketbase=集合物理名 vpd_xxx */ }
interface VpField { uid: string; alias: string; type?: string; revisedType?: string; sourceField?: string }
```

- `uid`/`name`/`type` 必填；`tables` 要有（否则无法绑定）。
- `type` vs `connectorType`：判别一律看 `connectorType`；`type` 是旧兼容字段（仅当 `connectorType` 缺失时用于动态源判定）。编辑器写出的值：文件类 `type:"file"`、接口类 `type`=`connectorType`（如 `api`）、pocketbase `type:"pocketbase"`、mock `type:"mock"`。手写静态数据对齐编辑器写 `type:"file", connectorType:"json"`。**别写 `"static"`**——它不在内置连接器清单，编辑器会当未知类型（`static` 仅由「冻结导出」内部生成）。
- **不变式**：`connection.tables[i].uid === connectionData[connUid][i].tableId`。

## 2. 连接器类型与 config

内置清单：

| connectorType | category | config 关键键 | 说明 |
|---|---|---|---|
| `pocketbase` | platform | `{ realtime, limit, sort, url, relations }` | **平台数据**：本系统 PocketBase 数据表（后端动态集合 `vpd_xxx`），实时订阅（SSE）或按 `refreshInterval` 轮询 |
| `mock` | local | `{ mock: { rowCount, fields } }` + `refreshInterval` | 模拟数据：按字段规则在前端生成，可定时刷新，适合实时监控/演示大屏 |
| `json` | file | `{ fileName, copyToRoot }` | 上传/粘贴 JSON。**手写静态数据首选**（字段 uid=列名） |
| `csv` | file | `{ fileName, copyToRoot }` | CSV |
| `excel` | file | `{ fileName, copyToRoot }` | Excel |
| `api` | http | `{ method, url, headers, queryParams, body, bodyType, auth, variables, dataPath, script }` | HTTP 取数；`dataPath` 定位响应里的数组 |
| `websocket` | http | `{ url, protocol, mode, dataPath, script }` | 长连接推送 |
| `webhook` | http | `{ url, responseMode, dataPath, script }` | 回调推送；`responseMode:"实时更新"` 且**未写 `refreshInterval`** 时按 2s 轮询——写了 `refreshInterval`（含 `0`）一律以它为准（`0`=不轮询） |
| `postMessage` | http | `{ linkId, responseMode, dataPath, script }` | 父窗口 `postMessage` 通信，按 `linkId` 匹配 |
| `mysql`/`sqlserver`/`pgsql`/`mongodb` | db | — | **当前未启用**，勿用 |

> ⚠️ **没有 `static` 连接器**（不在 `VP_CONNECTORS` 清单）。内联/静态数据一律用 `connectorType:"json"`。写 `"static"` 会让编辑器把它当未知类型（无类型图标、无法替换）。
>
> 接口类（api/websocket/webhook/postMessage）运行时**只更新该连接的第一张表** `connectionData[conn][0]`（`kP` 固定取 `[0].tableId`）；`mock` 的定时重生成同样只写首表。

文件/JSON 类连接器运行时不取数 —— 数据就是 `connectionData` 里的内联 rows。
`mock` 连接器会在预览/播放态按 `refreshInterval`（秒）定时重新生成首表 rows；`refreshInterval:0` 表示不自动刷新，但仍可被「刷新数据」动作或菜单手动触发。
⚠️ **启动即有一次取数/生成**：运行时对每个连接器先立即执行一次、再按 `refreshInterval` 挂 `setInterval`——即使 `refreshInterval:0`/缺省，mock 连接也会在启动时把首表 rows 重生成一遍（种子行被替换），api/webhook 连接同样启动即拉取一次。
⚠️ `VpConnection` 类型上声明了 `refreshIntervals?: Record<string, number>`（意图按表覆盖），但运行时 `connectionRuntime` 只读连接级 `refreshInterval`——当前版本设置 `refreshIntervals` 会被静默忽略。
> SDK 侧对应 `viewer.refreshData(connUid?)`：传 uid 定向刷新单连接，无参刷新全部可重取连接（`Bie` 全量）；均只写各连接首表。

### 2.0 pocketbase 平台数据连接（仅在部署了后端时可用）

数据表在编辑器「平台数据」里创建（后端动态建 PocketBase 集合）；工程 JSON 侧的形态：

- 连接：`type:"pocketbase", connectorType:"pocketbase", category:"platform"`；`config` 键都可省略走默认（`realtime:true` 实时订阅、`limit:500` 行数上限、`sort:"created"`、`relations` 关联展平映射）。
- 表：`tables[].source` = 集合物理名（`vpd_xxx`）；字段 `sourceField` = 字段物理名，行按 `sourceField → uid` 物化。
- 鉴权 token 由编辑器登录后注入，**不落入工程 JSON**；匿名查看端只能读「公开读取」的表。
- **手写生成的工程一般不要用它**（依赖目标环境有同名数据表）；演示/交付优先 `json`/`mock`。

## 2.1 在编辑器里查看 / 修改数据源（重要）

「连接数据」面板的编辑能力按连接器类型区分：

- **模拟/接口/流式类（mock / api / websocket / webhook / postMessage）** 显示「✎ 编辑数据源」铅笔按钮，点开可改连接配置。
- **JSON / CSV 文件类** 也显示铅笔按钮，点开后用 JSON 编辑器回填当前行数据；保存时就地更新首表数据，并保留 `connUid/tableUid/fieldUid`，已绑定字段不会失效。
- **Excel 和数据库类** 暂不支持就地编辑；需要删除后重新上传/创建。
- 任意类型点开表后，右侧「数据结构」可改字段**别名 / 类型**；「数据明细」是只读预览（≤100 行）。

改 JSON/CSV 源数据的两种方式：

1. 编辑器内：在「连接数据」面板点该数据源的铅笔 → 修改 JSON 行数组 → 保存。
2. 直接改工程文件里的 `connectionData[...].rows`（连同 `connections[].tables[].fields` 保持一致）→ 重新导入工程。

## 3. field 与 uid 约定（关键）

| 数据来源 | 字段 `uid` 如何取 | `sourceField` |
|---|---|---|
| Mock（模拟数据） | **uid = 字段规则 name**（= rows 键） | 无 |
| JSON/CSV/Excel（静态/文件） | **uid = 列名**（= alias） | 无 |
| API 自动解析 | uid = 列名 | 无 |
| API 手动 JSONPath | uid = alias | `sourceField` = JSONPath |
| WebSocket/Webhook/PostMessage | uid = `f_xxx` | `sourceField` = 外部键名 |
| PocketBase 平台数据 | uid = `f_xxx`（自动生成；编辑时按 `sourceField` 复用旧 uid 保住绑定） | `sourceField` = 字段物理名，`alias` = 显示名（表另有 `source`=集合物理名） |

字段 `type`：`string` / `number` / `array`（无一等公民 `date`）。

**行对象的键 = 字段 uid**：`rows[i][fieldUid]` 直接取值。所以：

- 静态/文件类：`rows: [{ "月份":"一月", "销售额":120 }]`，字段 uid 就是 `"月份"`/`"销售额"`。
- 流式：`rows: [{ "f_month":"一月", "f_sales":120 }]`，字段 uid 是 `f_month` 等，且字段定义里 `sourceField` 指向外部键（如 `"month"`）。
- ⚠️ **一个组件同次取数的字段必须来自同一张表**（如 chart-bar 的 `category-field` + `metric-fields`）：混表时 `DataBucket.getBucket` 不出桶、`getRows` 返 `[]` 并写 `status.error` 的 `multi-filed-error`，组件**静默无数据**。跨表数据先在宿主/接口侧合成一张表。

## 4. connectionData 对象

```ts
type VpConnectionData = Record<string /*connUid*/, VpDataTable[]>
interface VpDataTable {
  tableId: string            // == connection.tables[i].uid（必写）
  tableName?: string         // 注意是 tableName，不是 name；可省略=继承 connections 表名
  fields?: VpField[]         // 可省略=继承 connections 表 fields；仅在确需覆盖时写
  rows: Array<Record<string, unknown>>   // 键为字段 uid（必写）
}
```

- 静态/文件类：**必须**带 rows，否则组件空数据。
- 接口类：可先 `rows: []`，运行时拉取/推送填充（手写时给几行示例更直观）。
- 整个 `connectionData` 省略 → `{}`，静态工程会没数据。
- **精简**（仅编辑器导入）：只写 `tableId + rows`；`tableName/fields` 留给导入器从 `connections` 补齐。只有当数据字段与连接定义不同才显式写 `fields`（此时该份为准）。

## 5. 一个完整连接示例（静态 JSON）

```json
{
  "connections": [{
    "uid": "conn_sales", "name": "销售数据",
    "type": "file", "connectorType": "json", "category": "file",
    "config": { "copyToRoot": true, "fileName": "" },
    "tables": [{
      "uid": "tbl_sales", "name": "销售数据",
      "fields": [
        { "uid": "月份", "alias": "月份", "type": "string" },
        { "uid": "销售额", "alias": "销售额", "type": "number" },
        { "uid": "利润", "alias": "利润", "type": "number" }
      ]
    }]
  }],
  "connectionData": {
    "conn_sales": [{
      "tableId": "tbl_sales",
      "rows": [
        { "月份": "一月", "销售额": 120, "利润": 60 },
        { "月份": "二月", "销售额": 200, "利润": 95 },
        { "月份": "三月", "销售额": 150, "利润": 70 }
      ]
    }]
  }
}
```

- 上面 `connectionData` 用的是**精简写法**：只写 `tableId + rows`，`tableName/fields` 由 `connections` 的 `tbl_sales` 补齐。
- 等价的**完整写法**（与导入后内部结构一致，仍合法）会在该表里再带一份 `"tableName": "销售数据"` 和与 `connections` 相同的 `fields`。两种都能导入编辑器；SDK 直挂只认完整写法（见文首 ⚠️），给 SDK 用就写完整或挂载前 `transformData` 补齐。
- 组件绑定示例：`"category-field": [["conn_sales","tbl_sales","月份"]]`，`"metric-fields": [["conn_sales","tbl_sales","销售额"],["conn_sales","tbl_sales","利润"]]`。

## 6. mock 模拟数据连接

`mock` 适合做无后端、会随时间变化的实时监控示例。字段规则写在 `config.mock.fields`，导入后可在「连接数据」面板点铅笔用 UI 修改字段、行数与刷新间隔。

```json
{
  "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, "jitter": 60, "decimals": 0 },
        { "name": "CPU", "alias": "CPU", "kind": "wave", "base": 67, "amplitude": 24, "periodSec": 12, "jitter": 4, "decimals": 0 }
      ]
    }
  },
  "tables": [{
    "uid": "tbl_kpi",
    "name": "实时KPI",
    "fields": [
      { "uid": "在线用户", "alias": "在线用户", "type": "number" },
      { "uid": "CPU", "alias": "CPU", "type": "number" }
    ]
  }]
}
```

全部 `kind`（`mockData.ts` 的 `VpMockFieldKind`）：

- `int`：整数区间随机，支持 `min/max`（取整，不用 `decimals`）。
- `float`：小数区间随机，支持 `min/max/decimals`。
- `wave`：随时间正弦波动，支持 `base/amplitude/periodSec/jitter/decimals`（多行时按行号加相位偏移）。
- `walk`：基于上一帧随机游走，支持 `start/step/min/max/decimals`。
- `sequence`：按行号生成等差序列 `start + rowIndex*step`，支持 `start/step/decimals`。
- `enum`：从 `options` 数组随机取一。
- `bool`：按 `trueRatio`（默认 0.5）概率为 true。
- `const`：固定 `value`。
- `timestamp`：当前时间，`format` = `ms`（默认，毫秒数）/`iso`/`time`/`datetime`。
- `row-const` / `row-walk` / `row-wave`：按行号分别取 `values[rowIndex]` / `starts[rowIndex]` 作初值游走 / `rows[rowIndex]` 的 wave 参数——长表多测点（每行一条曲线）用这三个。

`connectionData` 仍建议放一帧初始 rows，便于编辑态预览；播放/预览态启动后会由 mock 运行时按规则替换。

## 7. postMessage 连接（与示例文件一致）

宿主用 `window.postMessage({ linkId, payload }, '*')` 推数据。字段用 `f_xxx` + `sourceField`：

```json
{
  "uid": "conn_pm", "name": "PostMessage数据源",
  "type": "postMessage", "connectorType": "postMessage",
  "config": { "linkId": "link_demo", "responseMode": "实时更新" },
  "tables": [{
    "eventName": "data", "eventParams": "", "name": "data", "uid": "tbl_pm",
    "fields": [
      { "uid": "f_month", "sourceField": "month", "alias": "month", "type": "string" },
      { "uid": "f_sales", "sourceField": "sales", "alias": "sales", "type": "number" }
    ]
  }]
}
```

`connectionData` 里 rows 用字段 uid 作键：`{ "f_month":"一月", "f_sales":16000 }`。

边界：配置了 `linkId` 才按 `{linkId, payload}` 匹配并取 `payload`；**未配置 `linkId` 时任意 `window message` 都直接当数据**写入该连接——多连接器或宿主页有其他 postMessage 流量时务必配 `linkId` 隔离。取到数据后仍走 `dataPath`/`script` 提取行。

## 8. dataRules（数据规则）

```ts
interface VpDataRule { uid: string; name: string; rules: VpDataRuleRule[][] }
interface VpDataRuleRule {
  connection?: string; table?: string; field: string
  func: string                 // = > < != >= <= ⊃ !⊃ ∅ !∅
  args?: unknown
  method?: 'some' | 'every' | 'first' | 'last' | 'custom'
  order?: number               // custom 时第几行
  source?: 'field' | 'param'   // 旧兼容
}
```

- `rules` 二维：**组间 OR，组内 AND**。多个条件之间也是 OR。
- `func` 全集：`= > < >= <= != ⊃(含) !⊃(不含) ∅(为空) !∅(非空)`；`=`/`!=` 宽松比较（`"1"`==`1`）；`∅`/`!∅` 不需要 `args`。
- `method`：`some`(任一行,默认)/`every`/`first`/`last`/`custom`(+`order`,1-based 取第 N 行)。
- `field` 可为字段 uid，或位置字段 `row`(行号)/`reverseRow`(倒序行号)/`count`(条数)。
- 用项目参数：`connection`/`table` 填 `"__vp_project_params__"`，`field` 填参数名。
- 不需要数据规则就给 `[]`。

### 8.1 项目参数（千人千面）

`project.params` 是工程级键值变量（如 `{ "theme": "dark", "region": "华东" }`），让同一份工程按用户/角色/场景呈现不同内容。消费：①数据规则按参数判断（上面 `__vp_project_params__` 写法）→ 驱动条件样式/多状态/conditionMeet 触发器；②交互动作公式 `{:参数名}` 插值。修改：交互动作 `editProjectParams` / SDK `ctx.setParams` 或挂载 `params` / 二开脚本 `this.project.setParams`。参数变化响应式重算重渲。生成静态演示工程时通常留 `{}`。

示例：

```json
"dataRules": [{
  "uid": "dc_high", "name": "销售额偏高",
  "rules": [
    [
      { "connection": "conn_sales", "table": "tbl_sales", "field": "销售额", "func": ">", "args": 150, "method": "some" }
    ]
  ]
}]
```

## 9. connection.tables 与 connectionData 的分工

| 关注点 | connections[].tables | connectionData[conn] |
|---|---|---|
| 表 id | `tables[].uid` | `tableId`（须相等，必写） |
| 表名 | `tables[].name`（真源） | `tableName?`（可省略=继承） |
| 字段 | `fields[]`（真源） | `fields?`（可省略=继承；覆盖时才写） |
| 行数据 | 无 | `rows[]`（必写） |
| 事件名/参数 | `eventName`/`eventParams`（流式） | 无 |

**字段元信息**（别名/类型/树展示）以 `connections[].tables[].fields` 为单一真源；**行值**取自 `connectionData`。

- 精简仅限编辑器导入：`connectionData` 只写 `tableId + rows`，导入时自动补齐 `tableName/fields`；SDK 挂载用完整写法。
- 例外（有意覆盖）：当某表数据字段确需不同（如改过 `revisedType`/别名）才在 `connectionData` 写 `fields`，此时以 `connectionData.fields` 为准。
- 不变式不变：`connections[].tables[i].uid === connectionData[connUid][i].tableId`；组件绑定的 `fieldUid` 至少能在 `connections`（或显式的 `connectionData.fields`）里解析到。
