事件处理
HMI 页面有三类事件:
- DOM 事件 — click / input / change / keydown / mouseenter (原生 HTML)
darra.*事件 — ready / offline / error / alarm (PLC Service 推送)- 自定义事件 (CustomEvent) — 跨组件通信 / 业务事件
本文档讲透三类事件在 Darra HMI 中的标准用法。
1. DOM 事件 (Alpine 语法糖)
Alpine 提供 @event 语法, 等价于 addEventListener:
<button @click="darra.write('M0.0', true)">启动</button>
<input @input="console.log($event.target.value)" />
<div @mouseenter="hover = true" @mouseleave="hover = false"
:class="hover ? 'highlighted' : ''">
hover me
</div>
<div @keydown.escape="closeDialog()" tabindex="0">
按 ESC 关闭
</div>
Alpine 事件修饰符
| 修饰符 | 等价 | 说明 |
|---|---|---|
.prevent | ev.preventDefault() | 阻止默认 |
.stop | ev.stopPropagation() | 阻止冒泡 |
.capture | { capture: true } | 捕获阶段 |
.once | { once: true } | 只触发一次 |
.passive | { passive: true } | 被动监听 (提性能) |
.debounce.300ms | — | 防抖 300ms |
.throttle.200ms | — | 节流 200ms |
.outside | — | 元素外点击 |
.window / .document | — | 挂在 window/document |
.ctrl / .shift / .alt / .meta | — | 组合键修饰 |
示例:
<button @click.prevent.stop="run()">受保护按钮</button>
<input @input.debounce.500ms="search($event.target.value)" />
<div @scroll.passive="onScroll()">大列表</div>
<div @click.outside="showMenu = false">菜单外点关闭</div>
<div @keydown.window.ctrl.s.prevent="save()">全局 Ctrl+S</div>
2. darra.* PLC 推送事件
连接生命周期
darra.on('ready', () => {
console.log('[HMI] WebSocket 已连接')
document.querySelector('.offline-banner').style.display = 'none'
})
darra.on('offline', () => {
console.warn('[HMI] 连接断开')
document.querySelector('.offline-banner').style.display = 'block'
document.querySelectorAll('button.writable').forEach(b => b.disabled = true)
})
darra.on('error', (err) => {
// err = { type:'write', var:'DB2.Setpoint', error:'not_in_whitelist' }
showToast(`写入失败: ${err.var} (${err.error})`, 'red')
})
报警
darra.on('alarm', (alarm) => {
// alarm = { level: 'error', source: '伺服2', message: '过载', timestamp: 1713429123000 }
playSound('alarm.mp3')
addToAlarmList(alarm)
if (alarm.level === 'error') flashScreen('red')
})
认证
darra.on('auth_ok', (msg) => {
// msg = { user, role, token }
console.log('登录成功:', msg.user, '角色:', msg.role)
location.href = '/local/'
})
主题变化
darra.on('themeChanged', (name) => {
// 切皮肤后重算 Chart.js 颜色
charts.forEach(c => { c.options.color = getCssVar('--darra-text'); c.update() })
})
3. 变量驱动事件 (变化触发)
// 变量跳变检测
let lastState = null
darra.bind('M0.0', (v) => {
if (lastState !== null && lastState !== v) {
console.log('M0.0 从', lastState, '变到', v)
if (v) onStart()
else onStop()
}
lastState = v
})
// 阈值事件
darra.bind('DB1.Temp', (v) => {
if (v > 80 && !overTempFired) {
overTempFired = true
fireOverTempEvent()
} else if (v < 75 && overTempFired) {
overTempFired = false
}
})
4. 自定义事件 (CustomEvent + darra.bus)
原生 CustomEvent (同一 DOM 节点)
// 发送
const ev = new CustomEvent('order-selected', { detail: { id: 123, name: 'ORD-001' } })
document.getElementById('order-list').dispatchEvent(ev)
// 监听
document.getElementById('order-list').addEventListener('order-selected', (e) => {
console.log('选中:', e.detail)
})
Alpine:
<div id="order-list" @order-selected="handleSelect($event.detail)">
...
</div>
darra.bus (全局事件总线)
// JsCode 顶部定义
darra.bus = darra.bus || {
_listeners: {},
on(evt, cb) { (this._listeners[evt] = this._listeners[evt] || []).push(cb) },
off(evt, cb) { this._listeners[evt] = (this._listeners[evt]||[]).filter(x => x !== cb) },
emit(evt, data) { (this._listeners[evt] || []).forEach(cb => { try { cb(data) } catch(e) { console.error(e) } }) }
}
// 模块 A 发
darra.bus.emit('order-confirmed', { id: 123 })
// 模块 B 收
darra.bus.on('order-confirmed', (data) => {
console.log('收到:', data)
})
5. 页面生命周期事件
页面加载
document.addEventListener('DOMContentLoaded', () => {
// DOM 完全解析, 此时 darra-plc.js 已加载
})
document.addEventListener('alpine:init', () => {
// Alpine 初始化前, 注册 data / magic / directive 最佳时机
})
页面卸载 / 切换
window.addEventListener('beforeunload', (e) => {
// 清理定时器
clearInterval(trendTimer)
// 关闭 IndexedDB
dbHandle?.close()
})
window.addEventListener('pagehide', () => {
// 手机浏览器切后台时触发
})
window.addEventListener('visibilitychange', () => {
if (document.hidden) pauseAnimations()
else resumeAnimations()
})
画面进出 (多页 HMI)
<darra-nav-button> 切换时走 location.href, 整页重载, 组件状态清零。若要保持状态, 用 SPA 路由或 sessionStorage:
// 页面离开前保存
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('page-state', JSON.stringify({ tab: currentTab, filter: currentFilter }))
})
// 新页面进入时读取
const saved = JSON.parse(sessionStorage.getItem('page-state') || '{}')
6. 定时器 (setInterval / setTimeout)
// 定时轮询 Historian 日 KPI (PLC 实时变量用订阅, 历史数据才用 REST)
const pollTimer = setInterval(async () => {
const kpi = await queryHistorian({ var: 'DB_KPI.Today', agg: 'last' })
updateKpiPanel(kpi)
}, 30000) // 30 秒
// 页面关闭清理
window.addEventListener('beforeunload', () => clearInterval(pollTimer))
// 一次性延迟
setTimeout(() => darra.write('M0.0', false), 500)
禁止: setInterval(() => darra.get('DB1.Temp'), 100) — 轮询缓存是多余的, 直接用 darra.bind 事件驱动。
7. 键盘/鼠标全局事件
// 全局快捷键
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 's') { e.preventDefault(); saveState() }
if (e.key === 'F11') toggleFullscreen()
if (e.key === 'Escape') closeDialog()
})
// 全局鼠标空闲检测 (5 分钟无操作回首页)
let idleTimer
function resetIdle() {
clearTimeout(idleTimer)
idleTimer = setTimeout(() => location.href = '/local/', 5 * 60 * 1000)
}
document.addEventListener('mousemove', resetIdle)
document.addEventListener('keydown', resetIdle)
document.addEventListener('touchstart', resetIdle)
resetIdle()
8. 触摸事件 (移动端)
<div @touchstart.passive="handleTouchStart($event)"
@touchmove.passive="handleTouchMove($event)"
@touchend="handleTouchEnd($event)">
可滑动区域
</div>
<script>
let startX, startY
function handleTouchStart(e) {
const t = e.touches[0]
startX = t.clientX
startY = t.clientY
}
function handleTouchEnd(e) {
const t = e.changedTouches[0]
const dx = t.clientX - startX
const dy = t.clientY - startY
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
if (dx > 0) onSwipeRight()
else onSwipeLeft()
}
}
</script>
详见 移动端 HMI。
9. 组合示例: 一键救火模式
<button @click="emergencyMode()">一键停机</button>
<script>
async function emergencyMode() {
if (!confirm('确认切入紧急停机?')) return
// 批量写 PLC
darra.writeGroup({
'M0.0': false, // 主启动 OFF
'M0.1': true, // 急停 ON
'DB2.SetpointSpeed': 0,
'DB2.SetpointTemp': 25
})
// 通知 Service 记审计
await fetch('/api/hmi/audit', {
method: 'POST',
body: JSON.stringify({ action: 'emergency_stop', ts: Date.now() })
})
// 触发业务事件
darra.bus.emit('emergency-activated', { ts: Date.now() })
// UI 反馈
document.body.classList.add('emergency-mode')
showToast('已进入紧急停机', 'red')
}
</script>
最佳实践
- 写入按钮配 confirm / long-press: 危险操作至少二次确认
- 所有 setInterval 必须 clear: 页面切换 / 关闭时
- avoid
oninput高频写 PLC: 用.debounce.300ms - darra 事件回调 try/catch: 避免一个异常阻塞其他
- 自定义事件走 bus: 比
window.xx = true全局变量优雅 - 键盘事件带 preventDefault: Ctrl+S 防止触发浏览器 "保存网页"