事件交互:触发器(Trigger) → 动作(Action)(options.interactions)
除了「数据交互」(筛选/选中/联动),appvisual 还有一套独立的事件交互系统:某个事件触发 → 执行一个或多个动作(改属性、切页面、打开链接、发/收行为事件…)。完整可导入示例见 data/数据交互/事件交互.vpproj.json。
持久化形态(写在元素 options.interactions 里)
interactions 是元素(页面/组件/子元素都行)的一个普通 option,形如:
"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 旧形态用「哈希键对象」(
interactionKeyFNV 值作键)已废弃,无需再算哈希;一律写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):
<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 §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、SDKcreateVpViewer都不调——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是播放态生命周期触发器:appearwatch[status.isMounted, status.appearTime](guardisVisible!==false && isMounted),status.appearTime在isPlaying(运行/预览态)由挂载钩子自动写——runtime viewer(createVpViewer)实例addWidget挂载即自动派发 appear 链。disappear同理 watchstatus.disappearTime。
basic-button的selectTrigger/unselectTrigger经真实选中态驱动:触发器 watchisSelected字段(immediate),setOption('checked',true)不写isSelected(checked 是 option 属性、isSelected 是运行态)、status.*直写无效;对内层按钮 DOMdispatchEvent(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 §6.8。 - 组态画板:
scadaWritePoint(写测点)、scadaSetOption(改图元选项):见 widgets/scada-2d.md §7.4。options 形状(官方工程里):{ "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,否则当字符串。例:
"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 平级):
{
"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。