跳到主要内容

事件处理

HMI 页面有三类事件:

  1. DOM 事件 — click / input / change / keydown / mouseenter (原生 HTML)
  2. darra.* 事件 — ready / offline / error / alarm (PLC Service 推送)
  3. 自定义事件 (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 事件修饰符

修饰符等价说明
.preventev.preventDefault()阻止默认
.stopev.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 防止触发浏览器 "保存网页"

相关文档