# 事件交互：触发器(Trigger) → 动作(Action)（`options.interactions`）

除了「数据交互」(筛选/选中/联动)，appvisual 还有一套独立的**事件交互**系统：**某个事件触发 → 执行一个或多个动作**（改属性、切页面、打开链接、发/收行为事件…）。完整可导入示例见 `data/数据交互/事件交互.vpproj.json`。

## 持久化形态（写在元素 `options.interactions` 里）

`interactions` 是元素（页面/组件/子元素都行）的一个普通 option，形如：

```jsonc
"options": {
  "interactions": {
    "version": 2,                         // 固定写 2
    "groups": [                           // ★ 触发分组数组（trigger 即身份，无需键）
      {
        "trigger": { "name": "click", "options": {} },
        "interactions": [                 // 该触发下的若干「交互项」(编辑器里的一行)
          {
            "alias": "点击→打开链接",
            "enable": true,
            "trigger": { "name": "click", "options": {} },   // trigger 副本(与分组同)
            "action": {                   // 单动作模式
              "name": "openUrl",
              "options": { "url": "https://...", "openWay": "_blank",
                           "start": "meanwhile", "delay": 0, "duration": 1 }
            }
          }
        ]
      }
    ]
  }
}
```

- 一个分组 = 一种触发器（`trigger.name` + 触发器自身 `options`）。同名但 options 不同（如不同 `eventName`）= 不同分组。
- 一个分组下可有多条「交互项」；同一项 `action`(单动作) 或 `actions[]`(多目标模式，trigger 声明了 triggerTargets 时)。
- 动作通用 options：`start`(`meanwhile` 与上一个同时 / `follow` 在其后)、`delay`(ms)、`duration`(ms)。⚠️**缺省（不写 `start`）也是串行新批次**：`startInteractions` 把相邻 `meanwhile` 项并入前一批并行，其余每项各自成批、`Promise` 链串行——**后项要等前项 `delay+duration` 全部跑完才开始计自己的 delay**（delay:600 项在前时，delay:0 项也到 ~600ms 才执行；写 `start:"meanwhile"` 则 ~0ms 并行）。`delay/duration` 由 rAF 帧驱动，页面不可见时停走。
- 目标元素引用统一写 `"element": [{ "targetPath": [...uid路径], "refType": "element" }]`。完整形态 `["screenUid","widgetUid"]`（子图元再加 `pieceUid`）；SDK / playground 也可只写 `["widgetUid"]`（组件 uid 全局唯一）。空/省略 → 目标 = 触发元素自身。

## ★ 分组靠 `trigger` 匹配（无需键 / 无需哈希）

运行时按 `group.trigger`（`name` + 该触发器自身 `options`）的**内容**匹配派发，不再用任何键。写 `groups` 数组时只要保证：

- **同一元素内每个 `trigger` 不重复**——同名且 options 相同只放一个分组，其下可挂多条交互项；
- 同名但 options 不同（如不同 `eventName` / 不同 `condition`）= **不同分组**，各占数组一项。

> v1 旧形态用「哈希键对象」（`interactionKey` FNV 值作键）已废弃，无需再算哈希；一律写 `version: 2` + `groups` 数组。

运行时也有对应 API（元素内核层，`handle.raw` 上）：`setInteractionsOption(groups)` 写回 `{version:2, groups}`、`getInteractionsOption()` 读出剥离 `version` 的 groups——**运行时可改写交互且即刻生效**（触发器每次派发时按当前 options 重新匹配分组）。

## VP-TSX 写法（helper 组装，与上面 JSON 同构）

TSX 里用 runtime 的 `interactions()/on()/act()` 组装（产物就是上面的 `{ version:2, groups }`，见 [vptsx.md](vptsx.md)）：

```tsx
<basic-button interactions={interactions(
  on('click',
    act('switchWindow', { windowUid: [{ targetPath: ['win_detail'] }], operate: 'open' }),
    act('sendBehaviorEvent', { element: [{ targetPath: ['screen_main', 'w_map'], refType: 'element' }], eventName: 'focusDevice' }),
  ),
)} />

// 触发器带参：on 的第一参传 { name, options }（⚠️ 页面级 timer 只在编辑器内预览生效，/view、SDK 静默不跑——见触发器清单下注）
<screen interactions={interactions(
  on({ name: 'timer', options: { mode: 'interval', interval: 5 } }, act('requestApi', { method: 'GET', url: 'https://…', resultMode: 'setData', connection: 'conn_x', table: 'tbl_x' })),
)} … />
```

导出代码（`page_export_tsx`）里 interactions 是 JSON 字面量而非 helper 调用——两种形态等价，改写时保持任一均可。

## 触发器清单（trigger.name）

| name | 别名 | 适用 | 触发器 options | payload（传给动作的 `{:字段}`） |
|---|---|---|---|---|
| `click` | 点击 | 组件 | — | 无 |
| `mouseEnter`/`mouseLeave` | 移入/移出 | 组件 | — | 无 |
| `appear`/`disappear` | 入场/离场 | 组件 | — | 无（带 pre/post，配合进/退场动画） |
| `receiveBehaviorEvent` | 接收二开事件 | 组件与页面（页面/窗口上仅编辑器内预览生效，见下注） | `eventName`(string) | 发送方带的 data |
| `itemClicked` | 点击某项/选中项 | 绝大多数数据组件（图表全系、表格、列表/分页/日历/时间选择、图标/图形/卡片、tabs/buttons、repeater、地图、视图容器/导航、轮播…） | — | 选中项维度（图表如 `{城市:"北京"}`，仅类目）；repeater=整行；分页={页码,每页条数} |
| `click-row`/`select-row`/`unselect-row`/`click-operate` | 表格行/操作 | 表格 | — | 行 payload：`{rowNumber,value,row:{别名:值},…}` |
| 地图触发器（14 个）：`marker-click`/`region-click`/`map-select`/`map-unselect`/`map-blank-click`/`map-click`/`map-dblclick`/`map-hover`/`map-contextmenu`/`map-menu-item`/`map-ready`/`map-view-change`/`map-source-loaded`/`map-error` | 地图要素/交互/视角/就绪/错误 | 2D 地图 | 部分可选目标图层 | 要素类 payload 为命中要素属性（`{:name}`/`{:value}`）；`map-view-change` 带视角、`map-menu-item` 带菜单项 |
| `scada-node-click`/`scada-node-dblclick`/`scada-link-click`/`scada-blank-click` | 组态图元/连线/空白 | 组态画板 | 可选目标图元 uid | `{nodeUid,name,shape,dataKey,value}`（见 [widgets/scada-2d.md](widgets/scada-2d.md) §7.4） |
| `viewChanged` | 切换视图时 | 视图容器/视图导航/轮播图 | — | `{index,label}`（任何来源切换都派发） |
| `timeChanged` | 时间变化 | 时间轴 basic-timescale | — | `{time}`（当前游标时间） |
| `switchChanged` | 开关切换时 | 开关 basic-switch | — | `{checked,value}`（value=开启值/关闭值归一后的字面量；接组态 `scadaWritePoint` 时「payload 字段」填 `value` 即写测点） |
| `mediaEnded` | 播放结束 | 视频 basic-video | — | `{src}`（配「播完切页/下一个」） |
| `conditionMeet` | 数据规则满足 | **页面**（仅编辑器内预览） | `condition`(string[]，dataRules uid) | 无 |
| `screenAppear` | 页面入场 | **页面**（仅编辑器内预览） | `appearSwitch`(`first`/`always`) | 无 |
| `timer` | 定时器 | **页面**（仅编辑器内预览） | `mode`(`interval`每隔一段/`at`每天时刻)、`interval`(秒,默认5)、`at`(`HH:mm:ss`)、`startDelay`(秒)、`maxCount`(0不限)、`onlyActiveScreen`(仅当前页运行) | `{time,count}`（供 requestApi 等动态值） |

> 触发器（除地图/表格自定义）多在**预览/播放态**才生效。`itemClicked` 的 payload 只含类目字段（如 `城市`），不含数值；要取数值用表格的 `click-row`（payload 含整行 `row`）或动态取值 `fromCurrentValue`。
>
> ⚠️ **页面/窗口级触发器只在编辑器内预览生效**：页面/窗口元素只有编辑器 store 的 `prepareScreen` 钩子会调 `initAfterConstructor`（→ `starTriggertListen`/`watchSelection`/`watchLinkage`）；**发布 `/view`（`RuntimeHost`→`buildRuntimeProject`）、独立 viewer、SDK `createVpViewer` 都不调**——`screen._listenTriggers` 恒空，写在页面/窗口 `options.interactions` 里的 `timer`/`screenAppear`（窗口「画面打开时」）/`conditionMeet`/`receiveBehaviorEvent` **静默不触发**，页面级 `select-sync-receive`/`cross-filter-receive` 开关在运行态也恒为关（交互链本身有效：手动 `startInteractions(getInteractionsByTrigger(...))` 可跑通）。组件级触发器（click/mouseEnter/appear/disappear/receiveBehaviorEvent/各组件自定义）正常监听。反过来也一样：`timer` 写到**组件**上同样静默——组件触发器集只有 `click/mouseEnter/mouseLeave/appear/disappear/receiveBehaviorEvent`+类型自定义，不含 `timer`。需要周期取数改用数据源的定时刷新（api 连接 `refreshInterval`）、SDK 侧 `setInterval` + `ctx.refreshData()`，或扩展脚本 `setInterval`；事件接收写在组件上。
>
> `mouseEnter` 的 DOM 链：组件 DOM 监听写 `status.mouseEnterTime` → 触发器 watch 该字段 → 派发（直接写 `status.mouseEnterTime` 同效）。
>
> **`click` 触发器的自动化通道**：`click` 触发器 watch `[status.clickTime, status.behaviorEventsTime?.click]`，三条路**都能**派发——① `handle.click()`（本质就是写 `status.behaviorEventsTime.click=Date.now()`）；② 直写 `status.clickTime` 或 `status.behaviorEventsTime.click`（Vue3 响应式对新键也触发）；③ 部分组件的真实 DOM click（`basic-text` 容器 dispatchEvent 可派发；`basic-button` 的 DOM click 走的是 select 链路、不写 click 字段）。注意 `status` 初始键集里没有 `clickTime`（只有 `behaviorEventsTime,error,isNew,isVisible,isMounted,isBehaviorReady,appearTime`），但写入后 watcher 照常触发。自动化验证交互链首选 `handle.click()`/`handle.emit(name,data)`。
>
> ⚠️ **`element` 目标寻址只认 uid 不认 name**：`changeOption`/`sendBehaviorEvent` 等动作的 `element:[{targetPath:[screenUid,widgetUid]}]` 经 `getElementByUID` 解析——**targetPath 段必须是 uid**（addWidget 自动生成的，如 `kpqvmtcqo4u1`），写 widget 的 `name` 会静默解析失败：触发器照常派发、action 拿到空目标 no-op，sink 保持原值，无任何报错。跨组件动作调试第一步先查 targetPath 是 uid。
>
> **`appear`/`disappear` 是播放态生命周期触发器**：`appear` watch `[status.isMounted, status.appearTime]`（guard `isVisible!==false && isMounted`），`status.appearTime` 在 `isPlaying`（运行/预览态）由挂载钩子自动写——runtime viewer（`createVpViewer`）实例`addWidget` 挂载即自动派发 appear 链。`disappear` 同理 watch `status.disappearTime`。
>
> **`basic-button` 的 `selectTrigger`/`unselectTrigger` 经真实选中态驱动**：触发器 watch `isSelected` 字段（immediate），`setOption('checked',true)` 不写 `isSelected`（checked 是 option 属性、isSelected 是运行态）、`status.*` 直写无效；对内层按钮 DOM `dispatchEvent(click)` 可派发（组件 click 处理器置 `isSelected` 并写 `status.behaviorEventsTime.selectTrigger`）。
>
> ⚠️ **合成 `dispatchEvent` 能否驱动自定义触发器取决于组件内部实现**：`basic-table` 的 `.vxe-body--row` 行 click 静默但 `.vxe-cell` 单元格 click **能**派发 `click-row`（vxe 事件委托处理合成事件）；`basic-text` 容器 click 可派发 `click`。但 `map-2d` 的 `marker-click`（容器+canvas click）、`scene-3d` 的 `scene3d-piece-click`（canvas click）、`basic-timeselect` 改值后的 `timeChanged` 仍静默——这些触发器需要组件内部计算产物（vjmap/scene3d 命中检测结果、值提交钩子），不是纯 DOM 事件可达。验证这类链路用 `handle` 方法或写对应 `status.*Time` 字段，勿只靠 DOM 注入。

## 动作清单（action.name）

| name | 别名 | 关键 options |
|---|---|---|
| `changeOption` | 修改设置 | `targetElement`(`searchElement`/`filterElementChildren`/`filterCurrentChildren`)、`element`、`optionKey`(属性名)、`<optionKey>`:静态值；或 `valueDynamic:true`+`valueMode`(`value`/`formula`/`fromCurrentValue`)+`valueFormula`；可 `isTween`+`easing`(数值/向量类型才可缓动)。⚠️写入**瞬时层** `setTransientOption`（`getOption` 能读到，`node.options` 持久层不变、不入历史/不随导出） |
| `switchState` | 切换状态 | `element`、`state`(状态 uid) |
| `switchPopup` | 打开/关闭弹窗 | `popupUid`(目标 `group-popup` 组件引用)、`operate`(`show`/`hide`，默认 `show`)——页面内轻弹层 |
| `switchWindow` | 打开画面（窗口） | `windowUid`(`[{targetPath:["窗口uid"]}]`，缺省=动作所在窗口)、`operate`(`open`/`close`/`toggle`)——开关 `project.windows[]` 工程级窗口（弹窗/抽屉/锚定窗口）；打开时自动带触发元素锚点（地图点位跟随） |
| `switchScreen` | 切换页面 | `screenUID`(`[{targetPath:["页面uid"]}]`) |
| `openUrl` | 打开链接 | `url`(支持 `{:param}`/`{:payload字段}` 插值)、`openWay`(`_blank`/`_self`) |
| `sendBehaviorEvent` | 发送行为事件 | `element`(目标)、`eventName`；**无 data 选项**——透传的是本条交互的触发器 payload（`click` 为空、`itemClicked` 为 `{别名:值}`、`click-row` 为行 payload）给目标的 `receiveBehaviorEvent`，并旁路通知宿主 `ctx.on`；`element` 解析不到时静默返回（宿主也收不到） |
| `transfer` | 传递触发 | `element`(目标)、`trigger`(代触发的触发器名)；⚠️**不要带 start/delay/duration**，否则匹配不到目标分组（这些会并入触发匹配的 options）。⚠️**该动作缺一次性守卫**（源码无 `elapsed>0` 早退，且每次 start 的 cycleGuard 默认新建）：命中后目标链在 `elapsed=0` 与结束帧**各执行一遍**——下游非幂等动作（sendBehaviorEvent 等）会派发两次（帧被节流时可能退化为一次）。目标链里若挂计数/累加类副作用注意去重 |
| `executeScript` | 执行脚本 | `commond`(由宿主二开 `onScript` 接管) |
| `requestApi` | 调用接口 | 出站 HTTP：`method`/`url`/`headers`(JSON 串)/`body`；`{{payload.x}}`/`{{参数名}}` 占位注入 payload 与项目参数；**结果落地** `resultMode`：`ignore`/`setParams`(`valuePath`+`paramKey` 写项目参数)/`setData`(`connection`+`table`+`dataPath` 写进数据源，同取数写回路径)/`emit`(`resultEventName` 派发事件驱动 `receiveBehaviorEvent`)。带超时/在途去重；周期轮询取数用 api 连接 `refreshInterval`（页面 `timer` 触发器只在编辑器内预览生效） |
| `editProjectParams` | 修改项目参数 | `params-key`(string[]) + `<key>-params-value` |
| `reloadData` | 刷新数据 | `connection`(连接 uid，空=全部连接)；目标元素被忽略。仅 api/webhook(有 url)/mock/pocketbase 真正重取，json/csv 等静态源无变化 |
| `writePayloadToEmbedded` | 写入内嵌数据集 | `table`、`overwrite`。⚠️ 当前为空实现：宿主未提供 `writeEmbeddedData`，只打 `console.warn`，勿依赖（写数据源用 `requestApi` 的 `resultMode:"setData"` 或 SDK `ctx.setData`） |
| `changeFullscreen`/`logoutVisitor` | 全屏控制/访客登出 | 进退全屏；`logoutVisitor` ⚠️ 当前为空实现（宿主未提供 `logoutVisitor`，只打 `console.warn`），勿依赖 |
| `appear`/`disappear` | 进/退场动画 | `effect`(`fade` 淡 / `fly` 飞+`direction`(`left`/`right`/`top`/`bottom`)+`distance`(px) / `enlarge` 缩放+`from-scale` / `rotate`+`rotate-turns` / `flicker` 闪+`flicker-times`)、`easing`(非 flicker)；时长取动作 `duration`(ms)。未知 `effect` 静默回落 `fade` |
| `custom` / `empty` | 自定义（二开注册）/ 空动作 | — |

**组件自定义动作**（目标元素指向该类型组件时可用）：

- 视图容器 `layout-section`：`openView`(`view`: 目标视图序号)、`nextView`/`prevView`(无参循环切换)。
- 2D 地图 17 个（`mapFlyTo`/`mapFitData`/`mapLayerVisible`/`mapSwitchBasemap`/`mapRefreshData`…）：见 [widgets/map-2d.md](widgets/map-2d.md) §6.8。
- 组态画板：`scadaWritePoint`(写测点)、`scadaSetOption`(改图元选项)：见 [widgets/scada-2d.md](widgets/scada-2d.md) §7.4。options 形状（官方工程里）：
  ```jsonc
  { "name": "scadaWritePoint", "options": {
    "element": [{ "targetPath": ["screen_main", "w_scada"], "refType": "element" }],  // 目标画板
    "key": "PUMP_STATE",          // 字面测点键（不支持 {:x} 公式插值）；值可经 payload-field 从 payload 取
    "value": "run",               // 字面值；填 "toggle" 则 0↔1 翻转
    "payload-field": "",          // 非空时改从 payload 该字段取值（覆盖 value）
    "start": "meanwhile", "delay": 0, "duration": 1   // 时序（与官方示例一致）
  } }
  ```
  `scadaSetOption`：`piece-uid`（留空取 payload.uid）+ `option` + `value`（JSON 优先解析），如 `sprite-stop-frame` 指定图像集第几张。
- 开关 `basic-switch`：`switchOn`/`switchOff`/`switchToggle`(置开/置关/切换，均派发 `switchChanged`)。⚠️ **`switchToggle` 经交互链调用是空操作**：动作 `start()` 无 `elapsed>0` 一次性守卫，`startInteraction` 的 `elapsed=0` 帧与结束帧各执行一遍 → 双 toggle 净零（`changeSeq` +2、checked 回原态）。`switchOn`/`switchOff` 幂等不受害。要"切换"请用 `switchOn`/`switchOff` 按条件分路，或 SDK 直调 `handle.raw.toggle()`。

### `changeOption` 动态取值（公式）

`valueMode:"formula"` + `valueFormula` 里用 `{:expr}` 插值，取值优先级：**先取 payload 字段**（`{:字段}` 直名 / `{:row.字段}` 点路径；命中要素数据即用，`{:value}` 亦优先取 payload 的 `value` 要素数值），payload 无该字段时 `{:value}` 回退为目标当前值，其余名按项目参数取。结果若为纯数字串则转 number，否则当字符串。例：

```jsonc
"action": { "name": "changeOption", "options": {
  "targetElement": "searchElement",
  "element": [{ "targetPath": ["screen_main","w_text"], "refType": "element" }],
  "optionKey": "text", "valueDynamic": true, "valueMode": "formula",
  "valueFormula": "已选中：{:城市}", "start": "meanwhile", "delay": 0, "duration": 1 } }
```

## 多状态(state) 与 `switchState`（状态切换 / 数据驱动 / 页面联动）

组件/页面可声明多个**状态(state)**，每个状态对一部分 option 做覆盖；运行时切到某状态即套用其覆盖（其余项回退基础值）。完整可导入示例见 `data/数据交互/多状态切换.vpproj.json`。

**持久化形态**（写在节点上，与 `options` 平级）：

```jsonc
{
  "uid": "w_status", "type": "basic-text",
  "options": { "text": "待机", "text-font": { "size": 30, "color": "#c9d1d9" }, "background-color": "#1b2233" },
  "states": [
    { "uid": "", "name": "待机", "followScreenStates": [], "followDataRules": [] },  // ★ 默认态 uid 必须是空串 ""
    { "uid": "cs_alarm", "name": "告警" },
    { "uid": "cs_ok", "name": "正常" }
  ],
  "stateOverrides": {                                  // 键=状态 uid（不含默认态 ""）；只写覆盖项
    "cs_alarm": { "text": "⚠ 告警", "text-font": { "size": 30, "color": "#ff5c5c" }, "background-color": "#3a1418" },
    "cs_ok":    { "text": "✔ 正常", "text-font": { "size": 30, "color": "#52c41a" }, "background-color": "#10301a" }
  }
}
```

要点：

- **`options` = 默认态(`uid:""`)的基础值**；`stateOverrides[stateUid]` 只放覆盖项，未覆盖的项回退 `options`。覆盖按**顶层键整体替换**（如改字体颜色要写完整 `text-font` 对象，不能只写半个）。
- `states` 必须含默认态 `{ "uid": "", ... }`，否则 `switchState` 切不回默认。
- 三种切换方式（引擎均有单测覆盖）：

| 方式 | 怎么写 | 何时切 |
|---|---|---|
| **① 主动切**（事件→动作） | `switchState` 动作：`{ "element":[{targetPath,refType:"element"}], "state":"<uid>" }`（`state:""` 回默认；目标可为组件或页面） | 触发器（如 `click`）命中时 |
| **② 数据驱动** | 状态加 `"useCurrentRuleField": true, "dataRuleField": <索引>, "dataRuleFunc": ">=", "dataRuleArgs": "80"`；组件 `options` 里配 `"state-rule-fields": [[connUid,tableUid,fieldUid], ...]`（索引指向其第 N 项），取该字段**首行值**比较 | 数据/参数变化即自动重算 |
| **③ 跟随页面态** | 状态加 `"followScreenStates": ["<页面状态uid>"]`；页面自身也声明 `states`，由按钮 `switchState`(目标=页面) 切页面态 | 页面切到列表内状态时自动切 |
| **③′ 跟随工程数据规则** | 状态加 `"followDataRules": ["<dataRules uid>"]` | 该数据规则满足时自动切 |

- 多个状态**按 `states` 顺序求值，首个命中生效**；都不命中时**保持当前态、不回退**（数据驱动场景把"常态"放默认态 `options` 即可）。
- 运算符同数据规则：`= > < >= <= != ⊃`。

## 自检（事件交互 + 多状态）

```
[ ] interactions 为 { "version": 2, "groups": [...] } 形态
[ ] groups 数组里每个 trigger 不重复（同名同 options 只一项）
[ ] 分组 trigger.name / 各 action.name 都在上面清单里
[ ] 目标 element 写成 [{targetPath:[screenUid,widgetUid], refType:"element"}]，uid 真实存在
[ ] changeOption 静态值键名 = optionKey 末段；动态值用 valueMode/valueFormula
[ ] transfer 动作 options 只留 element + trigger（不带 start/delay/duration）
[ ] conditionMeet/screenAppear/timer 写在「页面」节点的 options.interactions（只在编辑器内预览生效，/view、SDK 不跑）；其余写在组件上
[ ] switchWindow 的 windowUid 指向 project.windows[] 里的窗口；switchPopup 的目标是 group-popup 组件
[ ] requestApi 的 resultMode=setData 时 connection/table 在 connections 中存在；setParams 时 paramKey 有意义
[ ] 多状态：states 含默认态 {uid:""}；stateOverrides 键都是已声明的非默认状态 uid
[ ] switchState 的 state 是目标元素 states 里声明过的 uid（""=回默认）
[ ] useCurrentRuleField 状态：dataRuleField 索引在组件 state-rule-fields 范围内
[ ] followScreenStates 引用的 uid 在所属页面 states 中；followDataRules 引用 dataRules[].uid
```

> SDK 正式用法见 [platform/sdk.md](platform/sdk.md)。
